paint-brush
Como criar um mecanismo de UI orientado a servidor para Flutterpor@alphamikle
1,242 leituras
1,242 leituras

Como criar um mecanismo de UI orientado a servidor para Flutter

por Mike Alfa30m2024/06/13
Read on Terminal Reader

Muito longo; Para ler

A história por trás da criação do Nui - mecanismo Server Driven UI para Flutter, que faz parte de um projeto maior - Backendless CMS Nanc.
featured image - Como criar um mecanismo de UI orientado a servidor para Flutter
Mike Alfa HackerNoon profile picture
0-item
1-item

Olá!

Hoje vou mostrar como criar um mecanismo super-duper para UI orientada a servidor no Flutter , que é parte integrante de um CMS super-duper (é assim que seu criador, ou seja, eu, o posiciona). Você, é claro, pode ter uma opinião diferente, e ficarei feliz em discuti-la nos comentários.


Este artigo é o primeiro de dois (já três) da série. Neste veremos diretamente o Nui, e no próximo - quão profundamente o Nui está integrado ao Nanc CMS, e entre este e o próximo artigo haverá outro com uma enorme quantidade de informações sobre o desempenho do Nui.


Neste artigo, haverá muitas coisas interessantes sobre UI orientada a servidor, recursos de Nui (UI orientada a servidor Nanc), histórico de projetos, interesses egoístas e Doutor Estranho. Ah, sim, também haverá links para GitHub e pub.dev, então se você gostar e não se importar em gastar de 1 a 2 minutos do seu tempo - ficarei feliz em ter sua estrela e like .


Índice

  1. Introdução
  2. Razões para o Desenvolvimento
  3. Prova de conceito
  4. Sintaxe
  5. Editor de IDE
  6. Desempenho
  7. Componentes e criação de UI
  8. Parque infantil
  9. Interatividade e Lógica
  10. Transferência de dados
  11. Documentação
  12. Planos futuros

Uma pequena introdução

Já escrevi um artigo sobre Nanc, mas desde então, mais de um ano se passou e o projeto progrediu significativamente em termos de capacidades e "completude" e, o mais importante, foi lançado com documentação finalizada e sob o MIT licença.

Então, o que é Nanc?

É um CMS de uso geral que não arrasta consigo seu back-end. Ao mesmo tempo, não é algo como React Admin, onde para criar algo é necessário escrever toneladas de código.


Para começar a usar o Nanc, basta:

  1. Descreva as estruturas de dados que você deseja gerenciar por meio do CMS usando Dart DSL
  2. Escreva uma camada API que implemente a comunicação entre o CMS e seu backend


Além disso, o primeiro pode ser feito totalmente por meio da interface do próprio CMS – ou seja, você pode gerenciar estruturas de dados por meio da UI. O segundo pode ser ignorado se:

  1. Você está usando o Firebase
  2. Ou você está usando Supabase
  3. Ou você deseja brincar e executar o Nanc sem vinculá-lo a um back-end real - com um banco de dados local (por enquanto, essa função é desempenhada por um arquivo JSON ou LocalStorage)


Assim, em alguns cenários, você não precisará escrever uma única linha de código para obter um CMS para gerenciar qualquer conteúdo e dados. No futuro, o número desses cenários aumentará, digamos - mais GraphQL e RestAPI. Se você tiver ideias sobre o que mais seria possível implementar um SDK, ficarei feliz em ler as sugestões nos comentários.


A Nanc opera com entidades – também conhecidas como modelos, que no nível da camada de armazenamento de dados podem ser representadas como uma tabela (SQL) ou um documento (No-SQL). Cada entidade possui campos - uma representação de colunas do SQL ou os mesmos "campos" do No-SQL.


Um dos tipos de campo possíveis é o chamado tipo “Tela”. Ou seja, todo este artigo é o texto de apenas um campo do CMS. Ao mesmo tempo, arquitetonicamente se parece com isto - há uma biblioteca completamente separada ( na verdade, várias bibliotecas ) que, juntas, implementam o mecanismo de UI orientado a servidor chamado Nui. Essa funcionalidade está integrada ao CMS, além do qual muitos recursos adicionais são incluídos.


Com isso concluo a parte introdutória dedicada diretamente a Nanc e começo a história sobre Nui.

Como tudo começou

Isenção de responsabilidade: todas as coincidências são acidentais. Esta história é fictícia. Eu sonhei com isso.

Trabalhei em uma grande empresa em vários aplicativos ao mesmo tempo. Eles eram muito semelhantes, mas também tinham muitas diferenças.

Mas o que era completamente idêntico neles era o que posso chamar de mecanismo de artigos . Consistia em vários (5-10-15, não me lembro mais exatamente) milhares de linhas de código bastante amassado que processavam JSON do back-end. Esses JSONs eventualmente tiveram que se transformar em UI, ou melhor, em um artigo para ser lido em um aplicativo móvel.


Os artigos foram criados e editados usando o painel de administração, e o processo de adição de novos elementos foi muito, incrivelmente, extremamente doloroso e longo. Vendo esse horror, resolvi propor a primeira otimização - ter piedade dos pobres gestores de conteúdo e implementar para eles a funcionalidade de visualização de artigos em tempo real direto no navegador, no painel de administração.


Dito e feito. Depois de algum tempo, uma parte esquelética do aplicativo estava girando no painel de administração, economizando muito tempo dos gerentes de conteúdo na visualização das alterações. Se antes eles tinham que criar um link direto e, a cada alteração, abrir o dev build, seguir este link, aguardar os downloads e repetir tudo, agora eles poderiam simplesmente criar artigos e vê-los imediatamente.


Mas meu pensamento não parou por aí - fiquei muito irritado com esse mecanismo e com outros desenvolvedores, pois era possível determinar se eles precisavam adicionar algo a ele ou apenas limpar os estábulos de Augias .


Se fosse o último, o desenvolvedor estava sempre de bom humor nas reuniões – embora o cheiro... a câmera não consiga capturar isso.

Se fosse a primeira opção, o desenvolvedor ficava frequentemente doente, passava por terremotos, tinha um computador quebrado, dores de cabeça, impactos de meteoritos, depressão em estágio terminal ou uma overdose de apatia.


A expansão da funcionalidade do mecanismo também exigiu a adição de vários novos campos ao painel de administração para que os gerentes de conteúdo pudessem utilizar os novos recursos.


Olhando para tudo isto, tive um pensamento incrível: porque não criar uma solução geral para este problema? Uma solução que nos impediria de ajustar e expandir constantemente o painel de administração e o aplicativo para cada novo elemento. Uma solução que resolveria o problema de uma vez por todas! E aí vem o...

Pequeno plano sorrateiro e ganancioso

Pensei: "Posso resolver esse problema. Posso salvar a empresa muitas dezenas, senão centenas de milhares; mas a ideia pode ser valiosa demais para a empresa simplesmente dá-la como presente ."


Por presente, quero dizer que a proporção do valor potencial para a empresa difere do que a empresa me pagará na forma de salário em ordens de grandeza. É como se você fosse trabalhar em uma startup ainda cedo, mas com um salário menor do que o oferecido em alguma grande empresa e sem participação na empresa. E então a startup se torna um unicórnio, e eles te dizem: “Bem, cara, nós pagamos um salário para você”. E eles estariam certos!


Adoro analogias, mas muitas vezes me dizem que elas não são meu forte. É como se você fosse um peixe que gostasse de nadar no oceano, mas fosse um peixe de água doce.


E então - resolvi fazer uma prova de conceito (POC), no meu tempo livre, para não errar oferecendo algumas ideias que talvez nem sejam possíveis de implementar.

Prova de conceito

O plano original era usar uma biblioteca já existente para renderizar markdown, mas expandir seus recursos para que pudesse renderizar não apenas os elementos padrão da lista de markdown, mas também algo muito mais complexo. Os artigos não eram apenas textos com imagens. Havia também um belo design visual, reprodutores de áudio integrados e muito mais.


Passei 40 horas, contando de sexta à noite até segunda de manhã, para testar essa hipótese - quão extensível esta biblioteca é para novos recursos, quão bem tudo funciona em geral e o mais importante - se esta solução pode derrubar o notório motor do trono. A hipótese foi confirmada - depois de desmontar a biblioteca até os ossos e um pouco de patch, tornou-se possível registrar quaisquer elementos da UI por meio de palavras-chave ou construções de sintaxe especiais, tudo isso poderia ser facilmente expandido e, o mais importante, poderia realmente substituir o mecanismo de artigo . Cheguei em algum lugar em 15 horas. Os 25 restantes gastei finalizando o POC.


A ideia não era apenas substituir um motor por outro – não. A ideia era substituir todo o processo! O painel de administração não só permite criar artigos, mas também gerencia o conteúdo que fica visível no aplicativo. A ideia original era criar um substituto completo que não estivesse vinculado a um projeto específico, mas que permitisse gerenciá-lo. O mais importante é que essa substituição também deve fornecer um editor conveniente para esses mesmos artigos, para que você possa criá-los e ver imediatamente o resultado.


Para o POC, pensei que bastaria apenas fazer um editor. Parecia algo assim:

Editor de IU

Depois de 40 horas, eu tinha um editor de código funcional que consistia em uma mistura turbulenta de markdown e um monte de tags XML personalizadas (por exemplo, <container> ), uma visualização exibindo a UI desse código em tempo real e também o maior bolsas nos olhos que este mundo já viu. É importante notar também que o "editor de código" usado é outra biblioteca capaz de realçar sintaxe, mas o problema é que ele pode destacar markdown, também pode destacar XML, mas o realce de uma miscelânea quebra constantemente. Portanto, durante as 40 horas, você pode adicionar mais alguns para a codificação de macaco de uma quimera que fornecerá destaque de ambos em um frasco. É hora de perguntar: o que aconteceu a seguir?


Primeira demonstração

A seguir foi a demonstração. Reuni alguns gestores seniores, expliquei-lhes a minha visão para resolver o problema, o facto de ter confirmado esta visão na prática, e mostrei o que funciona e como, e quais as possibilidades que tem.


Os caras gostaram do trabalho. E havia um desejo de usá-lo. Mas também havia uma ganância corrosiva. Minha ganância. Eu não poderia simplesmente dar para a empresa assim? Claro que não. Mas eu também não planejei. A demo fez parte de um plano ousado onde eu os choquei com meu ofício, eles simplesmente não resistiram e estavam prontos para atender qualquer condição, apenas para usar esse desenvolvimento incrível, exclusivo e surpreendente. Não vou revelar todos os detalhes dessa história fictícia (!) , mas direi apenas que queria dinheiro. Dinheiro e férias. Férias remuneradas de um mês, além de dinheiro. Quanto dinheiro não é tão importante, só é importante que o valor corresponda ao meu salário e ao número 6.

Mas eu não era um temerário completamente imprudente.


Dormammu, vim negociar. E o negócio foi o seguinte - eu trabalho duas semanas inteiras no meu modo ( dormir 4 horas, trabalhar 20 horas ), finalizando o POC para um estado de "pode ser usado para fins de nosso aplicativo", e paralelamente a isso, eu implemento um novo recurso no aplicativo - uma tela inteira, usando essa ultra-coisa (para a qual essas duas semanas foram originalmente reservadas). E ao final de duas semanas, realizamos outra demonstração. Só que desta vez reunimos mais pessoas, até mesmo a alta administração da empresa, e se o que eles veem os impressiona e querem usar - o negócio está feito, eu realizo meus desejos e a empresa ganha uma super arma. Se eles não quiserem nada disso, estou pronto para aceitar o fato de ter trabalhado de graça durante essas duas semanas.


Pedra Furada (perto de Urubici)

Pois bem, a viagem para Urubici , que eu já havia planejado para meu mês de férias, infelizmente, nunca aconteceu. Os gerentes não se atreveram a concordar com tal audácia. E eu, baixando o olhar para o chão, fui talhar uma nova tela no “modo clássico”. Mas não existe tal história em que o personagem principal, derrotado pelo destino, não se levante e tente domesticar sua fera novamente.


Embora não... parece que existem: 1 , 2 , 3 , 4 , 5 .


Depois de assistir a todos esses filmes, decidi que isso era um sinal ! E que é ainda melhor assim - é uma pena vender um empreendimento tão promissor por algumas guloseimas aí ( quem estou enganando??? ), e continuarei desenvolvendo meu projeto ainda mais. E eu continuei. Mas já não são 40 horas nos fins de semana, apenas 15-20 horas por semana, num ritmo relativamente calmo.

Codificar ou não codificar?

Quebrar a quarta parede não é uma tarefa fácil. Assim como tentar inventar manchetes interessantes que façam o leitor continuar lendo e aguardar o final da história da empresa. Terminarei a história no segundo artigo. E agora, ao que parece, é hora de passar para a implementação, capacidades funcionais e tudo mais, o que, em teoria, deveria tornar este artigo técnico e o HackerNoon maior!

Sintaxe

A primeira coisa sobre a qual falaremos é a sintaxe. A ideia original da mistura era adequada para POC, mas, como a prática tem mostrado, a redução não é tão simples. Além disso, combinar alguns elementos de markdown nativos com elementos puramente Flutter nem sempre é consistente.


A primeira pergunta é: a imagem será ![Description](Link) ou <image> ?


Se for o primeiro - onde coloco vários parâmetros?

Se o segundo - por que então temos o primeiro?


A segunda questão são os textos. As possibilidades do Flutter para estilizar textos são ilimitadas. As possibilidades de remarcação são “mais ou menos”. Sim, você pode marcar o texto em negrito ou itálico, e até se pensou em usar essas construções ** / __ para estilizar. Depois, pensei em colocar tags <color="red"> text </color> no meio, mas isso é tão curvo e assustador que o sangue escorre dos olhos. Obter algum tipo de HTML, com sua própria sintaxe marginal, não era nada desejável. Além disso, a ideia era que esse código pudesse ser escrito até mesmo por gestores sem conhecimento técnico.


Passo a passo, removi a parte da quimera e consegui um supermutante markdown. Ou seja, obtivemos uma biblioteca corrigida para renderização de markdown, mas recheada de tags customizadas e sem suporte a markdown. Isto é, como se tivéssemos XML.


Sentei-me para pensar e experimentar outras sintaxes simples que existem. JSON é escória. Fazer uma pessoa escrever JSON em um editor Flutter torto é pegar um maníaco que vai querer te matar. E não se trata apenas disso, não me parece que JSON seja adequado para digitação por uma pessoa em geral, principalmente para UI - está crescendo constantemente para a direita, um monte de "" obrigatórios, não há comentários. YAML? Bem, talvez. Mas o código também irá rastejar lateralmente. Existem links interessantes, mas você não conseguirá muito apenas com a ajuda deles. TOML? Pf-ff.


Ok, afinal optei pelo XML. Pareceu-me, e ainda parece agora, que esta é uma sintaxe bastante "densa", muito adequada para UI. Afinal, ainda existem designers de layout HTML, e aqui tudo será ainda mais simples do que na web ( provavelmente ).


Em seguida, surgiu a questão - seria bom ter a possibilidade de algum destaque/completar código. Além de construções lógicas, tipo {{ user.name }} . Aí comecei a experimentar com Twig, Liquid, olhei alguns outros motores de template dos quais não me lembro mais. Mas me deparei com outro problema - é bem possível implementar parte do que foi planejado em um mecanismo padrão, digamos, Twig, mas definitivamente não funcionará para implementar tudo. E sim, é bom que haja preenchimento automático e destaque, mas eles só irão interferir se você implementar seus próprios novos recursos sobre a sintaxe padrão do Twig, que será necessária para o Flutter. Como resultado, com XML tudo deu muito certo, os experimentos com Twig / Liquid não deram resultados excelentes e, em certos momentos, até me deparei com a impossibilidade de implementar alguns recursos. Portanto, a escolha ainda permaneceu pelo XML. Falaremos mais sobre os recursos, mas por enquanto vamos nos concentrar no preenchimento automático e no realce, que eram tão tentadores no Twig/Liquid.


Ambiente de desenvolvimento integrado

A próxima coisa que quero dizer é que o Flutter tem entradas de texto distorcidas. Eles funcionam bem em formato móvel. Também é bom em formato desktop quando se trata de algo, bem, no máximo 5 a 10 linhas de altura. Mas quando se trata de um editor de código completo, onde esse editor é implementado no Flutter, você não consegue olhar para ele sem lágrimas. No Trello , onde acompanho todas as tarefas e escrevo notas e ideias, existe uma "tarefa":


Tarefa para alterar o editor de código da IU


Na verdade, quase desde o início do trabalho no projeto, tive em mente a ideia de substituir o editor de código Nui por algo mais adequado. Digamos - incorpore uma visualização da web com a parte Open Source do VS Code. Mas até agora minhas mãos não chegaram a isso, além disso, uma solução difícil, mas ainda funcional, para o problema da curvatura deste editor me veio à mente - usar seu próprio ambiente de desenvolvimento.


Isso é conseguido da seguinte forma - crie um arquivo com código UI (XML), de preferência com a extensão .html / .twig , abra o mesmo arquivo através do CMS - Web/Desktop/Local/Implantado - não importa. E abra o mesmo arquivo através de qualquer IDE, até mesmo através da versão web do VS Code. E pronto - você pode editar este arquivo em sua ferramenta favorita e ter uma visualização em tempo real diretamente no navegador ou em qualquer lugar.


Sincronização Nanc + IDE


Nesse cenário, você pode até ativar o preenchimento automático completo. No VS Code, existe a possibilidade de implementá-lo através de tags HTML customizadas. Porém, eu não uso VS Code, minha escolha é o IntelliJ IDEA e para esse IDE não existe mais uma solução tão simples (bom, pelo menos não existia, ou pelo menos não encontrei). Mas existe uma solução mais geral que funcionará aqui e ali - XML Schema Definition (XSD). Passei cerca de 3 noites tentando descobrir esse monstro, mas o sucesso nunca veio e, no final, abandonei esse assunto, deixando-o para tempos melhores.


Também é interessante que no final, depois de muitas iterações de experimentos, atualizações, digamos, do mecanismo responsável pela conversão de XML em widgets, obtivemos uma solução para a qual a linguagem não é particularmente importante. Assim como um portador de informações sobre a estrutura de sua UI, a escolha acabou recaindo sobre XML, mas ao mesmo tempo, você pode alimentá-lo com segurança em JSON, e até mesmo em forma binária - Protobuf compilado. E isso nos leva ao próximo tópico.


Desempenho

Nesta frase, o tamanho deste artigo será de 3.218 palavras. Quando comecei a escrever esta seção, para fazer tudo com alta qualidade, foi necessário escrever muitos casos de teste comparando o desempenho da renderização do Nui e do Flutter normal. Como já tive uma tela de demonstração implementada, totalmente criada no Nui:


Demonstração da tela Nalmart


foi necessário criar uma correspondência exata da tela nativamente (no contexto do Flutter, claro). Como resultado, demorou mais de 3 semanas, reescrevendo muito a mesma coisa, melhorando o processo de teste e obtendo números cada vez mais interessantes. E só o tamanho desta seção ultrapassou 3.500 palavras. Portanto, cheguei à ideia de que faz sentido escrever um artigo separado que será inteiramente dedicado à atuação do Nui, como um caso particular, e ao preço adicional que você terá que pagar se decidir usar UI orientada a servidor como abordagem.


Mas vou dar um pequeno spoiler: havia dois cenários principais para avaliar o desempenho que considerei - o momento da renderização inicial . É importante se você decidir implementar a tela inteira na UI orientada por servidor, e essa tela será aberta em algum lugar do seu aplicativo.


Portanto, se esta tela for muito pesada, mesmo uma tela nativa do Flutter demorará muito para ser renderizada, portanto, ao mudar para tal tela, principalmente se essa transição for acompanhada de uma animação, os atrasos serão visíveis. O segundo cenário é o tempo de quadro (FPS) com alterações dinâmicas na interface do usuário . Os dados mudaram - você precisa redesenhar algum componente. A questão é o quanto isso afetará o tempo de renderização, se afetará tanto que quando a tela for atualizada o usuário verá lags. E aqui está outro spoiler – na maioria dos casos, você não será capaz de dizer que a tela que você vê está completamente implementada no Nui. Se você incorporar um widget Nui em uma tela Flutter nativa normal (digamos, alguma área da tela que deve mudar de forma muito dinâmica no aplicativo), é garantido que você não será capaz de reconhecer isso. É claro que há quedas no desempenho. Mas eles são tais que não afetam o FPS mesmo em uma taxa de quadros de 120FPS - ou seja, o tempo de um quadro quase nunca excederá 8ms . Isto é verdade para o segundo cenário. Quanto ao primeiro, tudo depende do nível de complexidade da tela. Mas mesmo aqui a diferença será tal que não afetará a percepção e não tornará seu aplicativo uma referência para smartphones de usuários .


Abaixo estão três gravações de tela do Pixel 7a (Tensor G2, taxa de atualização da tela definida para 90 quadros (máximo para este dispositivo), taxa de gravação de vídeo de 60 quadros por segundo (máximo para configurações de gravação). A cada 500ms, a posição dos elementos em a lista é aleatória, a partir dos dados dos quais são construídas as 3 primeiras cartas, e após mais 500ms, o status do pedido é alterado para a próxima. Você consegue adivinhar qual dessas telas está implementada inteiramente no Nui?


PS O tempo de carregamento das imagens não depende da implementação, pois nesta tela, com qualquer implementação, existem muitas imagens Svg - todos os ícones, assim como logotipos de marcas. Todos os SVG (assim como as imagens normais) são armazenados no GitHub, como hospedagem, para que possam carregar bem lentamente, o que é observado em alguns experimentos.


YouTube:


Componentes Disponíveis - Como Criar UI

Ao criar o Nui, aderi ao seguinte conceito - é necessário criar uma ferramenta que, em primeiro lugar, os desenvolvedores do Flutter a considerem tão fácil de usar quanto criar aplicativos Flutter regulares. Portanto, a abordagem para nomear todos os componentes foi simples: nomeá-los da mesma forma que são nomeados no Flutter.


O mesmo se aplica aos parâmetros do widget - os escalares, como String , int , double , enum , etc., que, como parâmetro, não são configurados por si próprios. Esses tipos de parâmetros dentro do Nui são chamados de argumentos . E para parâmetros de classe complexos, como decoration no widget Container , chamados property . Esta regra não é absoluta, pois algumas propriedades são muito detalhadas e seus nomes foram simplificados. Além disso, para alguns widgets, a lista de parâmetros disponíveis foi ampliada. Por exemplo - para fazer um SizedBox ou Container quadrado, você pode passar apenas um argumento personalizado size , em vez de dois idênticos width + height .


Não darei uma lista completa dos widgets implementados, pois existem alguns deles (53 no momento). Resumindo - você pode implementar quase qualquer UI para a qual faria sentido usar a UI orientada a servidor como uma abordagem em princípio. Incluindo efeitos de rolagem complexos associados a Slivers .


Widgets implementados



Além disso, em relação aos componentes, vale observar o ponto de entrada ou widget para o qual você deverá passar o código XML da nuvem. No momento existem dois desses widgets - NuiListWidget e NuiStackWidget .


O primeiro, por design, deve ser usado caso seja necessário implementar a tela inteira. Nos bastidores, é um CustomScrollView contendo todos os widgets que serão analisados a partir do código de marcação original. Além disso, a análise, pode-se dizer, é "inteligente": como o conteúdo de CustomScrollView deveria ser slivers , então uma solução possível seria agrupar cada um dos widgets no fluxo em um SliverToBoxAdapter , mas isso teria um impacto extremamente negativo no desempenho. Portanto, os widgets são incorporados em seus pais da seguinte maneira - começando pelo primeiro, descemos na lista até encontrarmos uma sliver real . Assim que encontramos uma sliver - adicionamos todos os widgets anteriores a SliverList e adicionamos ao pai CustomScrollView . Assim, o desempenho de renderização de toda a UI será o mais alto possível, já que o número de slivers será mínimo. Por que é ruim ter muitas slivers em CustomScrollView ? A resposta está aqui .


O segundo widget - NuiStackWidget também pode ser usado em tela cheia - neste caso, vale lembrar que tudo que você criar será incorporado no Stack na mesma ordem. E também será necessário usar slivers explicitamente - ou seja, se você quiser uma lista de slivers - terá que adicionar CustomScrollView e já implementar a lista dentro dele.


O segundo cenário é a implementação de um pequeno widget que pode ser incorporado em componentes nativos. Digamos - fazer um cartão de produto que seja totalmente customizável por iniciativa do servidor. Parece um cenário muito interessante no qual você pode implementar todos os componentes da biblioteca de componentes usando Nui e usá-los como widgets regulares. Ao mesmo tempo, sempre haverá a oportunidade de alterá-los completamente sem atualizar o aplicativo.


É importante notar que NuiListWidget também pode ser usado como um widget local, e não como uma tela inteira, mas para este widget você precisará aplicar restrições apropriadas, como definir uma altura explícita para o widget pai.


Esta é a aparência de um counter app se fosse criado usando Flutter:

 import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Demo App'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <text size="32"> {{ page.counter }} </text> </column> </center> ''', pageData: { 'counter': _counter, }, ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }


E aqui está outro exemplo, apenas totalmente em Nui (incluindo lógica):

 import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } final DataStorage globalDataStorage = DataStorage(data: {'counter': 0}); final EventHandler counterHandler = EventHandler( test: (BuildContext context, Event event) => event.event == 'increment', handler: (BuildContext context, Event event) => globalDataStorage.updateValue( 'counter', (globalDataStorage.getTypedValue<int>(query: 'counter') ?? 0) + 1, ), ); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return DataStorageProvider( dataStorage: globalDataStorage, child: EventDelegate( handlers: [ counterHandler, ], child: MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Counter'), ), ), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="FF000000" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned> ''', pageData: {}, ), ), ); } }


Separe o código da UI para que haja destaque:

 <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="black" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned> 

Aplicativo Nui Counter com lógica Nui


Há também documentação interativa e abrangente que mostra informações detalhadas sobre quais argumentos e propriedades cada widget possui, bem como todos os seus valores possíveis. Para cada uma das propriedades, que também podem ter argumentos e outras propriedades, existe também uma documentação, com demonstração completa de todos os valores disponíveis. Além disso, cada um dos componentes contém um exemplo interativo no qual você pode ver a implementação deste widget ao vivo e brincar com ele alterando-o como desejar.

Parque Nanc

Nui está totalmente integrado ao Nanc CMS. Você não precisa usar o Nanc para usar o Nui, mas usar o Nanc pode lhe trazer vantagens, nomeadamente - a mesma documentação interativa, assim como o Playground, onde você pode ver os resultados do layout em tempo real, brincar com os dados que será usado nele. Além disso, não é necessário criar sua própria versão local do CMS, você pode administrar perfeitamente com a demonstração publicada, na qual você pode fazer tudo o que precisa.


Você pode fazer isso seguindo o link e clicando no campo Page Interface / Screen . A tela aberta pode ser utilizada como Playground, e clicando no botão Sync , você pode sincronizar o Nanc com seu IDE através de um arquivo com fontes, e toda a documentação está disponível clicando no botão Help .


PS Essas complexidades existem porque nunca encontrei tempo para fazer uma página separada explícita com documentação sobre os componentes do Nanc, bem como a impossibilidade de inserir um link direto para esta página.


Interatividade e Lógica

Seria muito inútil criar um mapeador comum de XML para widgets. É claro que isso também pode ser útil, mas haverá muito menos casos de uso. Não é a mesma coisa - componentes e telas totalmente interativos com os quais você pode interagir, que podem ser atualizados granularmente (ou seja, não todos de uma vez - mas em partes que precisam ser atualizadas). Além disso, esta IU precisa de dados. Que, dada a presença da letra S na frase Server-Driven UI, pode ser substituída diretamente no layout do servidor, mas você também pode fazer isso de forma mais bonita. E não arrastar uma nova parte do layout do backend para cada mudança na UI (Nui não é uma máquina do tempo que faz as melhores práticas do jQuery vibrarem).


Vamos começar com a lógica: variáveis e expressões computadas podem ser substituídas no layout. Digamos que um widget seja definido como <container color="{{ page.background }}"> extrairá sua cor diretamente dos dados passados para o "contexto pai" armazenado na variável background . E <aspectRatio ratio="{{ 3 / 4}}"> definirá o valor da proporção de aspecto correspondente para seus descendentes. Existem funções integradas, comparações e muito mais que podem ser usadas para construir UI com alguma lógica.


O segundo ponto é a modelagem . Você pode definir seu próprio widget diretamente no código da UI usando a tag <template id="your_component_name"/> . Ao mesmo tempo, todos os componentes internos deste template terão acesso aos argumentos passados para este template, o que permitirá a parametrização flexível de componentes customizados e depois reutilizá-los usando a tag <component id="your_component_name"/> . Dentro dos templates, você pode passar não apenas atributos, mas também outras tags/widgets, o que possibilita a criação de componentes reutilizáveis de qualquer complexidade.


Ponto três - "for loops". No Nui, há uma tag <for> integrada que permite usar iterações para renderizar os mesmos (ou vários) componentes várias vezes. Isso é conveniente quando há um conjunto de dados a partir do qual você precisa criar uma lista/linha/coluna de widgets.


Quarto - renderização condicional. No nível do layout, a tag <show> é implementada (houve uma ideia de chamá-la <if> ), que permite desenhar componentes aninhados ou não incorporá-los na árvore sob várias condições.


Ponto cinco - ações. Alguns componentes com os quais o usuário pode interagir podem enviar eventos . Que você pode controlar completamente como quiser. Digamos <inkWell onPressed="something"> - com tal declaração, este widget se torna clicável e seu aplicativo, ou melhor, algum EventHandler , será capaz de manipular esse evento e fazer algo. A ideia é que tudo relacionado à lógica seja implementado diretamente na aplicação, mas você pode implementar qualquer coisa. Faça alguns manipuladores genéricos que possam lidar com grupos de ações, como "ir para a tela"/"método de chamada"/"enviar evento analítico". Existem planos para implementar código dinâmico também, mas há nuances aqui. Para o Dart, existem formas de executar código arbitrário, mas isso afeta o desempenho e, além disso, a interoperabilidade desse código com o código da aplicação dificilmente é de 100%. Ou seja, ao criar lógica nesse código dinâmico, você encontrará constantemente algumas limitações. Portanto, este mecanismo precisa ser trabalhado com muito cuidado para ser realmente aplicável e útil.


O sexto ponto é a atualização local da UI. Isso é possível graças à tag <dataBuilder> . Esta tag (bloco subjacente) pode "olhar" para um campo específico e, quando mudar, redesenhará sua subárvore.


Dados

Inicialmente, segui o caminho de dois armazenamentos de dados - o "contexto pai" mencionado acima. Bem como "dados" - dados que podem ser definidos diretamente na UI, usando a tag <data> . Para ser honesto, não consigo me lembrar agora do argumento de por que foi necessário implementar duas maneiras de armazenar e transferir dados para a IU, mas não posso me criticar duramente por tal decisão.


Eles funcionam da seguinte maneira - o "contexto pai" é um objeto do tipo Map<String, dynamic> , passado diretamente para os widgets NuiListWidget / NuiStackWidget . O acesso a esses dados é possível pelo prefixo page :

 <someWidget value="{{ page.your.field }}"/>

Você pode se referir a qualquer coisa, em qualquer profundidade, incluindo arrays - {{ page.some.array.0.users.35.age }} . Se não existir tal chave/valor, você obterá null . As listas podem ser iteradas usando <for> .


A segunda forma - "dados" é um armazenamento de dados global. Na prática, este é um determinado Bloc localizado mais alto na árvore que NuiListWidget / NuiStackWidget . Ao mesmo tempo, nada impede de organizar seu uso em estilo local, passando sua própria instância de DataStorage por meio de DataStorageProvider .


Ao mesmo tempo, o primeiro método não é reativo - ou seja, quando os dados na page mudam, nenhuma UI será atualizada. Já que estes são, na verdade, apenas os argumentos do seu StatelessWidget . Se a fonte de dados da page for, digamos, seu próprio Bloc, que fornecerá um conjunto de valores para Nui...Widget - então, como acontece com um StatelessWidget normal, ele será completamente redesenhado com novos dados.


A segunda forma de trabalhar com dados é reativa. Se você alterar os dados em DataStorage , usando a API desta classe - o método updateValue , então isso chamará o método emit da classe Bloc, e se houver ouvintes ativos desses dados em sua UI - tags <dataBuilder> , então seu conteúdo será alterado de acordo, mas o restante da IU não será alterado.


Assim, obtemos duas fontes de dados potenciais - uma page muito simples e uma data reativos. Exceto pela lógica de atualização dos dados nessas fontes e pela reação da UI a essas atualizações, não há diferença entre elas.

Documentação

Deliberadamente não descrevi todas as nuances e aspectos do trabalho, pois seria uma cópia da documentação já existente. Portanto, se você estiver interessado em experimentar ou apenas aprender mais, seja bem-vindo aqui. Se algum aspecto do trabalho não estiver claro ou a documentação não cobrir algo, ficarei lisonjeado com sua mensagem indicando o problema:


Listarei brevemente alguns dos recursos que não são abordados neste artigo, mas estão disponíveis para você:

  • Criando suas próprias tags/componentes, com a capacidade de criar exatamente a mesma documentação interativa para eles, assim como para seus argumentos e propriedades com visualização ao vivo. É assim que é implementado, por exemplo, o componente para renderização de imagens SVG . Não faz sentido colocá-lo no núcleo do motor, porque nem todo mundo precisa dele, mas como uma extensão disponível para uso passando apenas uma variável - fácil e simples. Diretamente -um exemplo de implementação .


  • Uma enorme biblioteca embutida de ícones que pode ser expandida adicionando os seus próprios (aqui acabei sendo inconsistente, e "empurrado", a lógica era disponibilizar o máximo possível de ícones para uso imediatamente e não havia necessidade de atualize o aplicativo para usar novos ícones). Prontos para uso estão disponíveis: fluentui_system_icons , material_design_icons_flutter e remixicon . Você pode visualizar todos os ícones disponíveis usando Nanc , Page Interface / Screen -> Icons

  • Fontes personalizadas, incluindo Google Fonts prontas para uso


  • Convertendo XML para JSON/protobuf e usando-os como "fontes" para UI


Tudo isso e muito mais pode ser estudado na documentação .


Qual é o próximo?

O principal é descobrir a possibilidade de executar código dinamicamente com lógica. Este é um recurso muito interessante que permitirá expandir seriamente as capacidades do Nui. Além disso, você pode (e deve) adicionar os widgets restantes raramente usados, mas às vezes muito importantes, da biblioteca Flutter padrão. Para dominar o XSD, para que o preenchimento automático de todas as tags apareça no IDE (há uma ideia de gerar esse esquema diretamente da documentação da tag, então será fácil criá-lo para widgets customizados e estará sempre atualizado -date, e também existe a ideia de fazer uma DSL gerada em Dart, que pode depois ser convertida em XML/Json/Protobuf). Bem, e otimização adicional de desempenho - não é ruim agora, nada mal, mas pode ser ainda melhor, ainda mais próximo do Flutter nativo.


Isso é tudo o que eu tenho. No próximo artigo contarei detalhadamente sobre o desempenho do Nui, como criei casos de teste, quantas dezenas de rakes percorri nesse processo e quais números podem ser obtidos em quais cenários.


Se você estiver interessado em experimentar o Nui ou conhecê-lo melhor - por favor, dirija-se ao balcão de documentação . Além disso, se não for difícil, coloque uma estrela no GitHub e um like no pub.dev - não é difícil para você, mas para mim, um remador solitário neste barco enorme - é incrivelmente útil.