paint-brush
Cómo crear un motor de interfaz de usuario controlado por servidor para Flutterpor@alphamikle
966 lecturas
966 lecturas

Cómo crear un motor de interfaz de usuario controlado por servidor para Flutter

por Mike Alfa30m2024/06/13
Read on Terminal Reader

Demasiado Largo; Para Leer

La historia detrás de la creación de Nui: motor de interfaz de usuario controlado por servidor para Flutter, que forma parte de un proyecto más amplio: Backendless CMS Nanc.
featured image - Cómo crear un motor de interfaz de usuario controlado por servidor para Flutter
Mike Alfa HackerNoon profile picture
0-item
1-item

¡Hola!

Hoy les mostraré cómo crear un motor súper tonto para la interfaz de usuario controlada por servidor en Flutter , que es una parte integral de un CMS súper tonto (así es como lo posiciono su creador, es decir, yo). Por supuesto, es posible que usted tenga una opinión diferente y estaré encantado de comentarla en los comentarios.


Este artículo es el primero de dos (ya tres) de la serie. En este, veremos directamente a Nui, y en el siguiente, cuán profundamente está integrado Nui con Nanc CMS, y entre este y el próximo artículo habrá otro con una gran cantidad de información sobre el rendimiento de Nui.


En este artículo habrá muchas cosas interesantes sobre la interfaz de usuario basada en servidor, las capacidades de Nui (IU basada en servidor de Nanc), el historial del proyecto, los intereses egoístas y Doctor Strange. Ah, sí, también habrá enlaces a GitHub y pub.dev, así que si te gusta y no te importa dedicar 1 o 2 minutos de tu tiempo, estaré encantado de tener tu estrella y tu me gusta .


Tabla de contenido

  1. Introducción
  2. Razones para el desarrollo
  3. Prueba de concepto
  4. Sintaxis
  5. Editor IDE
  6. Actuación
  7. Creación de componentes y UI
  8. Patio de juegos
  9. Interactividad y Lógica
  10. Transferencia de datos
  11. Documentación
  12. Planes futuros

Una pequeña introducción

Ya escribí un artículo sobre Nanc, pero desde entonces ha pasado más de un año y el proyecto ha progresado significativamente en términos de capacidades y "integridad" y, lo más importante, se publicó con la documentación terminada y bajo el MIT. licencia.

Entonces, ¿qué es Nanc?

Es un CMS de propósito general que no arrastra consigo su backend. Al mismo tiempo, no es algo así como React Admin, donde para crear algo necesitas escribir toneladas de código.


Para empezar a utilizar Nanc, basta con:

  1. Describe las estructuras de datos que deseas administrar a través del CMS usando Dart DSL
  2. Escriba una capa API que implemente la comunicación entre el CMS y su backend


Además, lo primero se puede hacer completamente a través de la interfaz del propio CMS, es decir, puede administrar estructuras de datos a través de la interfaz de usuario. El segundo se puede omitir si:

  1. Estas usando Firebase
  2. O estás usando Supabase
  3. O desea jugar y ejecutar Nanc sin vincularlo a un backend real, con una base de datos local (por ahora, esta función la desempeña un archivo JSON o LocalStorage)


Por lo tanto, en algunos escenarios, no tendrá que escribir una sola línea de código para obtener un CMS para administrar su contenido y datos. En el futuro, la cantidad de estos escenarios aumentará, digamos, además de GraphQL y RestAPI. Si tiene ideas sobre para qué más sería posible implementar un SDK, estaré encantado de leer las sugerencias en los comentarios.


Nanc opera con entidades, también conocidas como modelos, que en el nivel de la capa de almacenamiento de datos se pueden representar como una tabla (SQL) o un documento (No-SQL). Cada entidad tiene campos: una representación de columnas de SQL o los mismos "campos" de No-SQL.


Uno de los tipos de campo posibles es el llamado tipo "Pantalla". Es decir, todo este artículo es el texto de un solo campo del CMS. Al mismo tiempo, arquitectónicamente se ve así: hay una biblioteca completamente separada ( en realidad, varias bibliotecas ), que juntas implementan el motor de interfaz de usuario controlado por servidor llamado Nui. Esta funcionalidad está integrada en el CMS, además de la cual se incluyen muchas funciones adicionales.


Con esto concluyo la parte introductoria dedicada directamente a Nanc y comienzo la historia sobre Nui.

Cómo todo empezó

Descargo de responsabilidad: todas las coincidencias son accidentales. Esta historia es ficticia. Lo soñé.

Trabajé en una gran empresa en varias aplicaciones a la vez. Eran en gran medida similares pero también tenían muchas diferencias.

Pero lo que era completamente idéntico en ellos era lo que puedo llamar el motor de artículos . Consistía en varios (5-10-15, ya no recuerdo exactamente) miles de líneas de código bastante arrugado que procesaba JSON desde el backend. Estos JSON finalmente tuvieron que convertirse en UI, o mejor dicho, en un artículo para leer en una aplicación móvil.


Los artículos se crearon y editaron mediante el panel de administración y el proceso de agregar nuevos elementos fue muy, increíblemente, extremadamente doloroso y largo. Al ver este horror, decidí proponer la primera optimización: tener piedad de los administradores de contenido deficientes e implementar para ellos la funcionalidad de vista previa de artículos en tiempo real directamente en el navegador, en el panel de administración.


Dicho y hecho. Después de un tiempo, una parte delgada de la aplicación estaba girando en el panel de administración, lo que ahorró a los administradores de contenido mucho tiempo en la vista previa de los cambios. Si antes tenían que crear un enlace profundo y luego, para cada cambio, abrir la compilación de desarrollo, seguir este enlace, esperar las descargas y luego repetir todo, ahora simplemente podían crear artículos y verlos de inmediato.


Pero mi pensamiento no se detuvo allí: estaba demasiado molesto con este motor y con otros desarrolladores, ya que era posible determinar si necesitaban agregarle algo o simplemente limpiar los establos de Augías .


Si fuera lo último, el desarrollador siempre estaba de buen humor en las reuniones, aunque el olor... la cámara no puede capturar eso.

Si era lo primero, el desarrollador a menudo estaba enfermo, vivía terremotos, tenía una computadora rota, dolores de cabeza, impactos de meteoritos, depresión en etapa terminal o una sobredosis de apatía.


Ampliar la funcionalidad del motor también requirió agregar numerosos campos nuevos al panel de administración para que los administradores de contenido pudieran utilizar las nuevas funciones.


Al ver todo esto, se me ocurrió una idea increíble: ¿por qué no crear una solución general a este problema? Una solución que nos evitaría modificar y ampliar constantemente el panel de administración y la aplicación para cada nuevo elemento. ¡Una solución que solucionaría el problema de una vez por todas! Y aquí viene el...

Pequeño plan furtivo y codicioso

Pensé: "Puedo resolver este problema. Puedo ahorrarle a la empresa muchas decenas, si no cientos de miles; pero la idea puede ser demasiado valiosa para que la empresa la regale ".


Por regalo me refiero a que la proporción del valor potencial para la empresa difiere en órdenes de magnitud de lo que la empresa me pagará en forma de salario. Es como si empezaras a trabajar en una startup en una etapa temprana, pero por un salario inferior al que te ofrecen en una gran empresa y sin una participación en la empresa. Y luego la startup se convierte en un unicornio y te dicen: "Bueno, amigo, te pagamos un salario". ¡Y ellos estarían en lo correcto!


Me encantan las analogías, pero a menudo me dicen que no son mi fuerte. Es como si fueras un pez y le gustara nadar en el océano, pero fueras un pez de agua dulce.


Y luego, decidí hacer una prueba de concepto (POC), en mi tiempo libre, para no equivocarme ofreciendo algunas ideas que quizás ni siquiera sean posibles de implementar.

Prueba de concepto

El plan original era utilizar una biblioteca ya preparada para renderizar Markdown pero expandir sus capacidades para que pudiera renderizar no solo los elementos estándar de la lista de Markdown sino también algo mucho más complejo. Los artículos no eran sólo textos con imágenes. También había un hermoso diseño visual, reproductores de audio integrados y mucho más.


Pasé 40 horas, contando desde el viernes por la noche hasta el lunes por la mañana, para probar esta hipótesis: qué tan extensible es esta biblioteca para nuevas funciones, qué tan bien funciona todo en general y, lo más importante, si esta solución puede derrocar al notorio motor del trono. La hipótesis se confirmó: después de desmontar la biblioteca hasta los huesos y aplicar algunos parches, fue posible registrar cualquier elemento de la interfaz de usuario mediante palabras clave o construcciones de sintaxis especiales, todo esto se pudo ampliar fácilmente y, lo más importante, realmente se pudo reemplazar el motor de artículos. . Llegué a algún lugar en 15 horas. Los 25 restantes los dediqué a finalizar el POC.


La idea no era sólo sustituir un motor por otro, no. ¡La idea era reemplazar todo el proceso! El panel de administración no sólo le permite crear artículos sino que también administra el contenido que es visible en la aplicación. La idea original era crear un reemplazo completo que no estuviera vinculado a un proyecto específico pero que permitiera gestionarlo. Lo más importante es que este reemplazo también debería proporcionar un editor conveniente para estos mismos artículos para que puedan crearse y ver inmediatamente el resultado.


Para la POC, pensé que sería suficiente con crear un editor. Se parecía a esto:

Editor de interfaz de usuario

Después de 40 horas, tenía un editor de código funcional que consistía en una mezcla turbulenta de rebajas y un montón de etiquetas XML personalizadas (por ejemplo, <container> ), una vista previa que mostraba la interfaz de usuario de este código en tiempo real y también el editor de código más grande. bolsas en los ojos que este mundo haya visto jamás. También vale la pena señalar que el "editor de código" utilizado es otra biblioteca capaz de resaltar la sintaxis, pero el problema es que puede resaltar las rebajas, también puede resaltar XML, pero el resaltado de una mezcolanza se rompe constantemente. Entonces, durante las 40 horas, puede agregar un par más para la codificación de mono de una quimera que resaltará ambos en una sola botella. Es hora de preguntar: ¿qué pasó después?


Primera demostración

Lo siguiente fue la demostración. Reuní a un par de altos directivos, les expliqué mi visión para resolver el problema, cómo confirmé esta visión en la práctica y les mostré qué funciona, cómo y qué posibilidades tiene.


A los chicos les gustó el trabajo. Y había ganas de usarlo. Pero también había una codicia persistente. Mi codicia. ¿No podría simplemente dárselo a la empresa así? Por supuesto que no. Pero tampoco lo planeé. La demostración fue parte de un plan atrevido en el que los sorprendí con mi arte, simplemente no pudieron resistirse y estaban listos para cumplir con cualquier condición, solo para usar este increíble, exclusivo y sorprendente desarrollo. No revelaré todos los detalles de esta historia ficticia (!) , pero solo diré que quería dinero. Dinero y vacaciones. Un mes de vacaciones pagadas, además de dinero. Cuánto dinero no es tan importante, solo es importante que la cantidad se correlacione con mi salario y el número 6.

Pero yo no era un temerario completamente imprudente.


Dormammu, he venido a negociar. Y el trato fue el siguiente: trabajo dos semanas completas en mi modo ( duermo 4 horas, trabajo 20 horas ), termino el POC hasta un estado de "puede usarse para los propósitos de nuestra aplicación" y, en paralelo, implemento una nueva característica en la aplicación: una pantalla completa que utiliza esta ultracosa (para la cual originalmente se asignaron estas dos semanas). Y al cabo de dos semanas, realizamos otra demostración. Sólo que esta vez reunimos a más personas, incluso a los altos directivos de la empresa, y si lo que ven les impresiona y quieren utilizarlo, el trato está cerrado, yo cumplo mis deseos y la empresa obtiene una súper arma. Si no quieren nada de esto, estoy dispuesto a aceptar el hecho de que trabajé gratis estas dos semanas.


Pedra Furada (cerca de Urubici)

Bueno, el viaje a Urubici , que ya tenía planeado para mis vacaciones de un mes, lamentablemente nunca se realizó. Los directivos no se atrevieron a aceptar semejante audacia. Y yo, bajando la mirada al suelo, fui a tallar una nueva pantalla a la "manera clásica". Pero no existe tal historia en la que el personaje principal, derrotado por el destino, no se levante de rodillas y vuelva a intentar domar a su bestia.


Aunque no... parece que los hay: 1 , 2 , 3 , 4 , 5 .


Después de ver todas estas películas, ¡decidí que esto era una señal ! Y que es aún mejor así: es una lástima vender un desarrollo tan prometedor por algunas cosas buenas ( ¿a quién engaño??? ), y continuaré desarrollando mi proyecto. Y continué. Pero ya no 40 horas los fines de semana, sólo 15-20 horas a la semana, a un ritmo relativamente tranquilo.

¿Codificar o no codificar?

Romper la cuarta pared no es tarea fácil. Al igual que tratar de encontrar titulares interesantes que hagan que el lector continúe leyendo y espere cómo terminará la historia con la empresa. Terminaré la historia en el segundo artículo. Y ahora parece que es el momento de pasar a la implementación, las capacidades funcionales y todo eso, lo que, en teoría, debería hacer que este artículo sea técnico y HackerNoon mejor.

Sintaxis

Lo primero de lo que hablaremos es de sintaxis. La mezcolanza de ideas original era adecuada para POC, pero como ha demostrado la práctica, la reducción no es tan simple. Además, combinar algunos elementos nativos de rebajas con elementos puramente de Flutter no siempre es coherente.


La primera pregunta es: ¿la imagen será ![Description](Link) o <image> ?


Si es el primero, ¿dónde coloco un montón de parámetros?

Si es el segundo, ¿por qué entonces tenemos el primero?


La segunda pregunta son los textos. Las posibilidades de Flutter para diseñar textos son ilimitadas. Las posibilidades de rebajas son "regulares". Sí, puede marcar el texto en negrita o cursiva, e incluso se pensó en utilizar estas construcciones ** / __ para darle estilo. Luego hubo pensamientos de empujar etiquetas <color="red"> texto </color> en el medio, pero esto es tan curvo y arrastrado que la sangre fluye de los ojos. Obtener algún tipo de HTML, con su propia sintaxis marginal, no era nada deseable. Además, la idea era que este código pudiera ser escrito incluso por gerentes sin conocimientos técnicos.


Paso a paso, eliminé la parte de la quimera y obtuve un supermutante rebajado. Es decir, tenemos una biblioteca parcheada para representar rebajas, pero repleta de etiquetas personalizadas y sin soporte para rebajas. Es decir, como si tuviéramos XML.


Me senté a pensar y experimentar con otras sintaxis simples que existen. JSON es escoria. Hacer que una persona escriba JSON en un editor Flutter torcido es conseguir un maníaco que querrá matarte. Y no se trata solo de eso, no me parece que JSON sea adecuado para que una persona lo escriba en general, especialmente para la interfaz de usuario: crece constantemente hacia la derecha, un montón de "" obligatorios, no hay comentarios. ¿YAML? Bien quizás. Pero el código también se arrastrará hacia los lados. Hay enlaces interesantes, pero no se puede lograr mucho sólo con su ayuda. TOML? Pf-ff.


Bien, después de todo me decidí por XML. Me pareció, y me sigue pareciendo ahora, que se trata de una sintaxis bastante "densa", muy adecuada para la interfaz de usuario. Al fin y al cabo, los diseñadores de formato HTML todavía existen y aquí todo será incluso más sencillo que en la web ( probablemente ).


A continuación, surgió la pregunta: sería bueno tener la posibilidad de resaltar o completar el código. Además de construcciones lógicas, algo así como {{ user.name }} . Luego comencé a experimentar con Twig, Liquid, miré algunos otros motores de plantillas que ya no recuerdo. Pero me encontré con otro problema: es muy posible implementar parte de lo planeado en un motor estándar, digamos Twig, pero definitivamente no funcionará para implementar todo. Y sí, es bueno que habrá autocompletado y resaltado, pero solo interferirán si agrega sus propias funciones nuevas a la sintaxis estándar de Twig, que será necesaria para Flutter. Como resultado, con XML todo salió muy bien, los experimentos con Twig / Liquid no dieron resultados sobresalientes y, en ciertos puntos, incluso me encontré con la imposibilidad de implementar algunas funciones. Por lo tanto, la elección seguía siendo XML. Hablaremos más sobre las funciones, pero por ahora, centrémonos en el autocompletado y el resaltado, que eran tan tentadores en Twig/Liquid.


IDE

Lo siguiente que quiero decir es que Flutter tiene entradas de texto torcidas. Funcionan bien en formato móvil. También es bueno en formato de escritorio cuando se trata de algo, bueno, un máximo de 5 a 10 líneas de altura. Pero cuando se trata de un editor de código completo, donde este editor está implementado en Flutter, no puedes mirarlo sin lágrimas. En Trello , donde hago un seguimiento de todas las tareas y escribo notas e ideas, existe una "tarea":


Tarea para cambiar el editor de código UI


De hecho, casi desde el principio de trabajar en el proyecto, tuve en mente la idea de reemplazar el editor de código Nui por algo más adecuado. Digamos: incrustar una vista web con la parte de código abierto de VS Code. Pero hasta ahora, mis manos no han llegado a esto, además, se me ocurrió una solución difícil pero aún funcional al problema de la curvatura de este editor: usar su propio entorno de desarrollo.


Esto se logra de la siguiente manera: cree un archivo con código UI (XML), idealmente con la extensión .html / .twig , abra el mismo archivo a través del CMS - Web / Escritorio / Local / Implementado - no importa. Y abre el mismo archivo a través de cualquier IDE, incluso a través de la versión web de VS Code. Y listo, puedes editar este archivo en tu herramienta favorita y obtener una vista previa en tiempo real directamente en el navegador o en cualquier lugar.


Sincronización Nanc + IDE


En tal escenario, incluso puedes atornillar el autocompletado completo. En VS Code existe la posibilidad de implementarlo mediante etiquetas HTML personalizadas. Sin embargo, no uso VS Code, mi elección es IntelliJ IDEA y para este IDE ya no existe una solución tan simple (bueno, al menos no la había, o al menos no la encontré). Pero existe una solución más general que funcionará tanto allí como allí: la definición de esquema XML (XSD). Pasé unas 3 noches tratando de descubrir este monstruo, pero el éxito nunca llegó y, al final, abandoné este asunto y lo dejé para tiempos mejores.


También es interesante que al final, después de muchas iteraciones de experimentos, actualizaciones, digamos, el motor responsable de convertir XML en widgets, obtuvimos una solución para la cual el lenguaje no es particularmente importante. Como portador de información sobre la estructura de su interfaz de usuario, la elección finalmente recayó en XML, pero al mismo tiempo puede alimentarlo de forma segura con JSON e incluso en formato binario: Protobuf compilado. Y esto nos lleva al siguiente tema.


Actuación

En esta oración, el tamaño de este artículo será de 3218 palabras. Cuando comencé a escribir esta sección, para hacer todo de manera cualitativa, fue necesario escribir muchos casos de prueba que compararan el rendimiento del renderizado de Nui y Flutter normal. Como ya tenía implementada una pantalla de demostración, completamente creada en Nui:


Demostración de pantalla de Namart


era necesario crear una coincidencia exacta de la pantalla de forma nativa (en el contexto de Flutter, por supuesto). Como resultado, tomó más de 3 semanas, reescribir mucho lo mismo, mejorar el proceso de prueba y obtener números cada vez más interesantes. Y el tamaño de esta sección por sí sola superó las 3500 palabras. Por lo tanto, se me ocurrió la idea de que tiene sentido escribir un artículo aparte, que estará total y completamente dedicado al rendimiento de Nui, como caso particular, y al precio adicional que tendrás que pagar si decides utilizarlo. La interfaz de usuario basada en servidor como enfoque.


Pero haré un pequeño spoiler: hubo dos escenarios principales que consideré para evaluar el rendimiento: el momento del renderizado inicial . Es importante si decide implementar la pantalla completa en la interfaz de usuario controlada por el servidor, y esta pantalla se abrirá en algún lugar de su aplicación.


Entonces, si esta pantalla es muy pesada, incluso una pantalla Flutter nativa tardará mucho en renderizarse, por lo que al cambiar a dicha pantalla, especialmente si esta transición va acompañada de una animación, los retrasos serán visibles. El segundo escenario es el tiempo de fotograma (FPS) con cambios dinámicos en la interfaz de usuario . Los datos han cambiado: es necesario volver a dibujar algún componente. La pregunta es cuánto afectará esto al tiempo de renderizado, si afectará tanto que cuando se actualice la pantalla, el usuario verá retrasos. Y aquí hay otro spoiler: en la mayoría de los casos, no podrás darte cuenta de que la pantalla que ves está completamente implementada en Nui. Si incrusta un widget de Nui en una pantalla Flutter nativa normal (por ejemplo, algún área de la pantalla que debería cambiar de manera muy dinámica en la aplicación), tiene la garantía de que no podrá reconocerlo. Por supuesto, hay caídas en el rendimiento. Pero son tales que no afectan los FPS ni siquiera a una velocidad de fotogramas de 120 fps, es decir, el tiempo de un fotograma casi nunca excederá 8ms . Esto es cierto para el segundo escenario. En cuanto al primero, todo depende del nivel de complejidad de la pantalla. Pero incluso aquí la diferencia será tal que no afectará a la percepción y no convertirá su aplicación en un referente para los usuarios de smartphones .


A continuación se muestran tres grabaciones de pantalla de Pixel 7a (Tensor G2, la frecuencia de actualización de la pantalla se configuró en 90 cuadros (máximo para este dispositivo), velocidad de grabación de video de 60 cuadros por segundo (máximo para la configuración de grabación). Cada 500 ms, la posición de los elementos en la lista es aleatoria, a partir de cuyos datos se construyen las primeras 3 tarjetas, y después de otros 500 ms, el estado del pedido cambia al siguiente. ¿Podrás adivinar cuál de estas pantallas está implementada completamente en Nui?


PD: El tiempo de carga de las imágenes no depende de la implementación, ya que en esta pantalla, con cualquier implementación, hay muchas imágenes SVG, todos los íconos, así como logotipos de marcas. Todos los archivos SVG (así como las imágenes normales) se almacenan en GitHub, como alojamiento, por lo que pueden cargarse con bastante lentitud, lo que se observa en algunos experimentos.


YouTube:


Componentes disponibles: cómo crear una interfaz de usuario

Al crear Nui, me adherí al siguiente concepto: es necesario crear una herramienta que, en primer lugar, a los desarrolladores de Flutter les resulte tan fácil de usar como crear aplicaciones Flutter normales. Por lo tanto, el enfoque para nombrar todos los componentes fue simple: nombrarlos de la misma manera que se nombran en Flutter.


Lo mismo se aplica a los parámetros de los widgets: los escalares, como String , int , double , enum , etc., que, como parámetro, no están configurados por sí mismos. Este tipo de parámetros dentro de Nui se denominan argumentos . Y a parámetros de clase complejos, como decoration en el widget Container , llamado propiedad . Esta regla no es absoluta ya que algunas propiedades son demasiado detalladas, por lo que sus nombres se han simplificado. Además, para algunos widgets, se ha ampliado la lista de parámetros disponibles. Por ejemplo, para crear un SizedBox o un Container cuadrado, puede pasar sólo un argumento personalizado size , en lugar de dos idénticos width + height .


No daré una lista completa de los widgets implementados, ya que hay bastantes (53 en este momento). En resumen, puede implementar casi cualquier interfaz de usuario para la que, en principio, tendría sentido utilizar la interfaz de usuario basada en servidor como enfoque. Incluyendo complejos efectos de desplazamiento asociados con Slivers .


Widgets implementados



Además, en cuanto a los componentes, cabe destacar el punto de entrada o widget al que tendrás que pasar el código XML de la nube. Por el momento existen dos widgets de este tipo: NuiListWidget y NuiStackWidget .


El primero, por diseño, debe usarse si necesita implementar toda la pantalla. Debajo del capó, hay un CustomScrollView que contiene todos los widgets que se analizarán a partir del código de marcado original. Además, se podría decir que el análisis es "inteligente": dado que el contenido de CustomScrollView debería ser slivers , entonces una posible solución sería envolver cada uno de los widgets en la secuencia en un SliverToBoxAdapter , pero esto tendría un impacto extremadamente negativo. sobre el rendimiento. Por lo tanto, los widgets se integran en su padre de la siguiente manera: comenzando desde el primero, bajamos en la lista hasta encontrar un sliver real. Tan pronto como encontramos un sliver , agregamos todos los widgets anteriores a SliverList y lo agregamos al CustomScrollView principal. Por lo tanto, el rendimiento al renderizar toda la interfaz de usuario será lo más alto posible, ya que la cantidad de slivers será mínima. ¿Por qué es malo tener muchas slivers en CustomScrollView ? La respuesta está aquí .


El segundo widget, NuiStackWidget también se puede utilizar como pantalla completa; en este caso, vale la pena tener en cuenta que todo lo que cree se incrustará en la Stack en el mismo orden. Y también será necesario usar slivers explícitamente, es decir, si desea una lista de slivers , deberá agregar CustomScrollView y ya implementar la lista dentro de él.


El segundo escenario es la implementación de un pequeño widget que puede integrarse en componentes nativos. Digamos: crear una tarjeta de producto que sea completamente personalizable por iniciativa del servidor. Parece un escenario muy interesante en el que puedes implementar todos los componentes de la biblioteca de componentes usando Nui y usarlos como widgets normales. Al mismo tiempo, siempre existirá la oportunidad de cambiarlos por completo sin actualizar la aplicación.


Vale la pena señalar que NuiListWidget también se puede usar como un widget local, y no en toda la pantalla, pero para este widget, deberá aplicar las restricciones apropiadas, como establecer una altura explícita para el widget principal.


Así es como se vería una counter app si se creara con 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), ), ); } }


Y aquí hay otro ejemplo, solo completamente en Nui (incluyendo la 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 el código de la interfaz de usuario para que quede resaltado:

 <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> 

Aplicación Nui Counter con lógica Nui


También hay documentación interactiva y completa que muestra información detallada sobre qué argumentos y propiedades tiene cada widget, así como todos sus valores posibles. Para cada una de las propiedades, que también pueden tener argumentos y otras propiedades, también hay documentación, con una demostración completa de todos los valores disponibles. Además de esto, cada uno de los componentes contiene un ejemplo interactivo en el que podrás ver la implementación de este widget en vivo y jugar con él cambiándolo como quieras.

Parque Infantil Nanc

Nui está muy estrechamente integrado en Nanc CMS. No es necesario usar Nanc para usar Nui, pero usar Nanc puede brindarle ventajas, a saber, la misma documentación interactiva, así como Playground, donde puede ver los resultados del diseño en tiempo real y jugar con los datos. que se utilizará en él. Además, no es necesario crear su propia versión local del CMS; puede gestionarlo con la demostración publicada, en la que puede hacer todo lo que necesita.


Puede hacerlo siguiendo el enlace y luego haciendo clic en el campo Page Interface / Screen . La pantalla abierta se puede utilizar como Playground y, al hacer clic en el botón Sincronizar , puede sincronizar Nanc con su IDE a través de un archivo con fuentes, y toda la documentación está disponible haciendo clic en el botón Ayuda .


PD: Estas complejidades existen porque nunca encontré el tiempo para crear una página separada explícita con documentación sobre los componentes en Nanc, así como la imposibilidad de insertar un enlace directo a esta página.


Interactividad y Lógica

Sería demasiado inútil crear un asignador normal de XML a widgets. Esto, por supuesto, también puede resultar útil, pero habrá muchos menos casos de uso. No es lo mismo: componentes y pantallas completamente interactivos con los que puede interactuar, que puede actualizar de forma granular (es decir, no todos a la vez, sino en partes que necesitan actualización). Además, esta interfaz de usuario necesita datos. Lo cual, teniendo en cuenta la presencia de la letra S en la frase UI basada en servidor, se puede sustituir directamente en el diseño del servidor, pero también se puede hacer de forma más bonita. Y no arrastrar una nueva parte del diseño desde el backend para cada cambio en la interfaz de usuario (Nui no es una máquina del tiempo que trae las mejores prácticas de jQuery a flutter).


Comencemos con la lógica: las variables y las expresiones calculadas se pueden sustituir en el diseño. Digamos que un widget se define como <container color="{{ page.background }}"> extraerá su color directamente de los datos pasados al "contexto principal" almacenado en la variable background . Y <aspectRatio ratio="{{ 3 / 4}}"> establecerá el valor de relación de aspecto correspondiente para sus descendientes. Hay funciones integradas, comparaciones y mucho más que se pueden usar para crear una interfaz de usuario con cierta lógica.


El segundo punto son las plantillas . Puede definir su propio widget directamente en el código de la interfaz de usuario utilizando la etiqueta <template id="your_component_name"/> . Al mismo tiempo, todos los componentes internos de esta plantilla tendrán acceso a los argumentos pasados a esta plantilla, lo que permitirá una parametrización flexible de componentes personalizados y luego reutilizarlos usando la etiqueta <component id="your_component_name"/> . Dentro de las plantillas, puede pasar no solo atributos sino también otras etiquetas/widgets, lo que hace posible crear componentes reutilizables de cualquier complejidad.


Punto tres: "bucles for". En Nui, hay una etiqueta <for> incorporada que le permite usar iteraciones para representar el mismo (o varios) componentes varias veces. Esto es conveniente cuando hay un conjunto de datos a partir del cual necesita crear una lista/fila/columna de widgets.


Cuarto: representación condicional. En el nivel de diseño, se implementa la etiqueta <show> (hubo la idea de llamarla <if> ), que le permite dibujar componentes anidados o no incrustarlos en el árbol bajo diversas condiciones.


Punto cinco: acciones. Algunos componentes con los que el usuario puede interactuar pueden enviar eventos . Que puedes controlar completamente como quieras. Digamos, <inkWell onPressed="something"> : con dicha declaración, se puede hacer clic en este widget y su aplicación, o mejor dicho, algún EventHandler , podrá manejar este evento y hacer algo. La idea es que todo lo relacionado con la lógica se implemente directamente en la aplicación, pero puedes implementar cualquier cosa. Cree algunos controladores genéricos que puedan manejar grupos de acciones, como "ir a la pantalla"/"método de llamada"/"enviar evento de análisis". También hay planes para implementar código dinámico, pero aquí hay matices. Para Dart, hay formas de ejecutar código arbitrario, pero esto afecta el rendimiento y, además, la interoperabilidad de este código con el código de la aplicación apenas es del 100%. Es decir, al crear lógica en este código dinámico, constantemente encontrarás algunas limitaciones. Por lo tanto, este mecanismo debe elaborarse con mucho cuidado para que sea realmente aplicable y útil.


El sexto punto es la actualización de la interfaz de usuario local. Esto es posible gracias a la etiqueta <dataBuilder> . Esta etiqueta (Bloque debajo del capó) puede "mirar" un campo específico y, cuando cambie, volverá a dibujar su subárbol.


Datos

Inicialmente, seguí el camino de dos almacenes de datos: el "contexto principal" mencionado anteriormente. Además de "datos", datos que se pueden definir directamente en la interfaz de usuario, utilizando la etiqueta <data> . Para ser honesto, ahora no recuerdo el argumento de por qué fue necesario implementar dos formas de almacenar y transferir datos a la interfaz de usuario, pero no puedo criticarme duramente por tal decisión.


Funcionan de la siguiente manera: el "contexto principal" es un objeto de tipo Map<String, dynamic> , pasado directamente a los widgets NuiListWidget / NuiStackWidget . El acceso a estos datos es posible a través de la page de prefijo:

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

Puede hacer referencia a cualquier cosa, con cualquier profundidad, incluidas las matrices: {{ page.some.array.0.users.35.age }} . Si no existe dicha clave/valor, obtendrá null . Las listas se pueden iterar usando <for> .


La segunda forma: "datos" es un almacén de datos global. En la práctica, se trata de un determinado Bloc ubicado más arriba en el árbol que NuiListWidget / NuiStackWidget . Al mismo tiempo, nada impide organizar su uso en un estilo local, pasando su propia instancia de DataStorage a través de DataStorageProvider .


Al mismo tiempo, el primer método no es reactivo, es decir, cuando los datos de page cambian, ninguna interfaz de usuario se actualizará. Dado que estos son, de hecho, solo los argumentos de su StatelessWidget . Si la fuente de datos de page es, digamos, su propio Bloc, que le dará un conjunto de valores a Nui...Widget , entonces, como ocurre con un StatelessWidget normal, se volverá a dibujar completamente con nuevos datos.


La segunda forma de trabajar con datos es reactiva. Si cambia los datos en DataStorage , usando la API de esta clase, el método updateValue , esto llamará al método emit de la clase Bloc, y si hay oyentes activos de estos datos en su interfaz de usuario, etiquetas <dataBuilder> , entonces su contenido se cambiará en consecuencia, pero el resto de la interfaz de usuario no se modificará.


Por lo tanto, obtenemos dos fuentes de datos potenciales: una page muy simple y data reactivos. Excepto por la lógica de actualizar los datos en estas fuentes y la reacción de la interfaz de usuario a estas actualizaciones, no hay diferencia entre ellas.

Documentación

Deliberadamente no describí todos los matices y aspectos del trabajo, ya que resultaría ser una copia de la documentación ya existente. Por lo tanto, si está interesado en probar o simplemente aprender más, bienvenido aquí. Si algún aspecto del trabajo no está claro o la documentación no cubre algo, me halagará su mensaje indicando el problema:


Enumeraré brevemente algunas de las funciones que no se tratan en este artículo, pero que están disponibles para usted:

  • Crear sus propias etiquetas/componentes, con la capacidad de crear exactamente la misma documentación interactiva para ellos, al igual que para sus argumentos y propiedades con vista previa en vivo. Así se implementa, por ejemplo, el componente para renderizar imágenes SVG . No tiene sentido introducirlo en el núcleo del motor, porque no todo el mundo lo necesita, sino como una extensión disponible para usar pasando solo una variable: fácil y simple. Directamente:un ejemplo de implementación .


  • Una enorme biblioteca de íconos incorporada que se puede expandir agregando los suyos propios (aquí resultó ser inconsistente y "empujado", la lógica era hacer que tantos íconos como fuera posible estuvieran disponibles para su uso de inmediato y no había necesidad de actualizar la aplicación para usar nuevos íconos). Están disponibles de fábrica: fluentui_system_icons , material_design_icons_flutter y remixicon . Puede ver todos los iconos disponibles usando Nanc , Page Interface / Screen -> Icons

  • Fuentes personalizadas, incluidas Google Fonts listas para usar


  • Convertir XML a JSON/protobuf y usarlos como "fuentes" para la interfaz de usuario


Todo esto y mucho más se puede estudiar en la documentación .


¿Que sigue?

Lo principal es descubrir la posibilidad de ejecutar código dinámicamente con lógica. Esta es una característica muy interesante que le permitirá ampliar seriamente las capacidades de Nui. Además, puede (y debe) agregar los widgets restantes que rara vez se usan, pero a veces muy importantes, de la biblioteca estándar de Flutter. Para dominar XSD, de modo que el autocompletado para todas las etiquetas aparezca en el IDE (existe una idea de generar este esquema directamente desde la documentación de la etiqueta, entonces será fácil crearlo para widgets personalizados y siempre estará actualizado). -date, y también existe la idea de crear un DSL generado en Dart, que luego se puede convertir a XML/Json/Protobuf). Bueno, y optimización adicional del rendimiento: no está mal en este momento, no está nada mal, pero puede ser incluso mejor, incluso más cercano al Flutter nativo.


Eso es todo lo que tengo. En el próximo artículo, contaré con gran detalle sobre el rendimiento de Nui, cómo creé casos de prueba, cuántas docenas de rastrillos pasé en este proceso y qué números se pueden obtener en qué escenarios.


Si está interesado en probar Nui o conocerlo mejor, diríjase al mostrador de documentación . Además, si no es difícil, pon una estrella en GitHub y un me gusta en pub.dev ; no es difícil para ti, pero para mí, un remero solitario en este enorme barco, es increíblemente útil.