Bonjour!
Aujourd'hui, je vais vous montrer comment créer un moteur super-duper pour l'interface utilisateur pilotée par serveur dans Flutter , qui fait partie intégrante d'un CMS super-duper (c'est ainsi que son créateur, c'est-à-dire moi, le positionne). Bien sûr, vous pouvez avoir un avis différent, et je me ferai un plaisir d'en discuter dans les commentaires.
Cet article est le premier des deux (déjà trois) de la série. Dans celui-ci, nous examinerons directement Nui, et dans le suivant - à quel point Nui est intégré à Nanc CMS, et entre cet article et le prochain, il y en aura un autre avec une énorme quantité d'informations sur les performances de Nui.
Dans cet article, il y aura beaucoup de choses intéressantes sur l'interface utilisateur pilotée par le serveur, les capacités de Nui (Nanc Server-Driven UI), l'historique du projet, les intérêts égoïstes et Doctor Strange. Oh oui, il y aura également des liens vers GitHub et pub.dev, donc si vous l'aimez et que cela ne vous dérange pas de passer 1 à 2 minutes de votre temps, je serai heureux d'avoir votre étoile et d'aimer .
J'ai déjà écrit un article sur Nanc, mais depuis lors, plus d'un an s'est écoulé depuis et le projet a considérablement progressé en termes de capacités et "d'exhaustivité", et surtout - il a été publié avec une documentation terminée et sous le MIT Licence.
Il s’agit d’un CMS à usage général qui n’entraîne pas son backend avec lui. En même temps, ce n'est pas quelque chose comme React Admin, où pour créer quelque chose, vous devez écrire des tonnes de code.
Pour commencer à utiliser Nanc, il suffit de :
De plus, la première peut être effectuée entièrement via l'interface du CMS lui-même, c'est-à-dire que vous pouvez gérer les structures de données via l'interface utilisateur. La seconde peut être ignorée si :
Ainsi, dans certains scénarios, vous n’aurez pas besoin d’écrire une seule ligne de code pour obtenir un CMS permettant de gérer vos contenus et vos données. À l'avenir, le nombre de ces scénarios augmentera, disons - ainsi que GraphQL et RestAPI. Si vous avez des idées sur quoi d'autre il serait possible d'implémenter un SDK, je serai heureux de lire les suggestions dans les commentaires.
Nanc fonctionne avec des entités - c'est-à-dire des modèles, qui, au niveau de la couche de stockage de données, peuvent être représentées sous forme de table (SQL) ou de document (No-SQL). Chaque entité possède des champs - une représentation de colonnes de SQL, ou les mêmes "champs" de No-SQL.
L'un des types de champs possibles est ce que l'on appelle le type "Screen". Autrement dit, l’intégralité de cet article est le texte d’un seul champ du CMS. En même temps, du point de vue architectural, cela ressemble à ceci : il existe une bibliothèque complètement séparée ( en fait plusieurs bibliothèques ), qui implémentent ensemble le moteur d'interface utilisateur piloté par le serveur appelé Nui. Cette fonctionnalité est intégrée au CMS, sur laquelle de nombreuses fonctionnalités supplémentaires sont ajoutées.
Avec cela, je conclus la partie introductive dédiée directement à Nanc et commence l'histoire de Nui.
Avertissement : toutes les coïncidences sont accidentelles. Cette histoire est fictive. J'en ai rêvé.
J'ai travaillé dans une grande entreprise sur plusieurs applications à la fois. Ils étaient en grande partie similaires mais présentaient également de nombreuses différences.
Mais ce qui était complètement identique chez eux, c'était ce que je peux appeler le moteur d'articles . Il se composait de plusieurs (5-10-15, je ne me souviens plus exactement) des milliers de lignes de code plutôt froissé qui traitaient JSON depuis le backend. Ces JSON ont finalement dû se transformer en UI, ou plutôt en article à lire dans une application mobile.
Les articles ont été créés et édités à l'aide du panneau d'administration, et le processus d'ajout de nouveaux éléments a été très, incroyablement, extrêmement pénible et long. Voyant cette horreur, j'ai décidé de proposer la première optimisation - avoir pitié des pauvres gestionnaires de contenu et implémenter pour eux la fonctionnalité de prévisualisation des articles en temps réel directement dans le navigateur, dans le panneau d'administration.
Dit et fait. Après un certain temps, une partie maigre de l'application tournait dans le panneau d'administration, ce qui faisait gagner beaucoup de temps aux gestionnaires de contenu lors de la prévisualisation des modifications. Si, auparavant, ils devaient créer un lien profond, puis, pour chaque modification, ouvrir la version de développement, suivre ce lien, attendre les téléchargements, puis tout répéter, ils pourraient désormais simplement créer des articles et les voir immédiatement.
Mais ma réflexion ne s'est pas arrêtée là : j'étais trop ennuyé par ce moteur , et par les autres développeurs car il était possible de déterminer s'ils devaient y ajouter quelque chose ou simplement nettoyer les écuries d'Augias .
Dans ce dernier cas, le développeur était toujours de bonne humeur lors des réunions, même si l'odeur... la caméra ne peut pas la capturer.
Dans le premier cas, le développeur était souvent malade, vivait des tremblements de terre, avait un ordinateur en panne, des maux de tête, des impacts de météorites, une dépression terminale ou une surdose d'apathie.
L'extension des fonctionnalités du moteur nécessitait également l'ajout de nombreux nouveaux champs au panneau d'administration afin que les gestionnaires de contenu puissent utiliser les nouvelles fonctionnalités.
En regardant tout cela, j'ai été frappé par une pensée incroyable : pourquoi ne pas créer une solution générale à ce problème ? Une solution qui nous éviterait de peaufiner et d'élargir constamment le panneau d'administration et l'application pour chaque nouvel élément. Une solution qui résoudrait le problème une fois pour toutes ! Et voici le...
J'ai pensé : "Je peux résoudre ce problème. Je peux économiser à l'entreprise plusieurs dizaines, voire centaines de milliers ; mais l'idée est peut-être trop précieuse pour que l'entreprise la donne simplement en cadeau ."
Par cadeau, j'entends que le ratio de la valeur potentielle pour l'entreprise diffère de ce que l'entreprise me versera sous forme de salaire par ordre de grandeur. C'est comme si vous alliez travailler très tôt dans une startup, mais pour un salaire inférieur à celui qui vous est proposé dans une grande entreprise, et sans participation dans l'entreprise. Et puis la startup devient une licorne, et ils vous disent : "Eh bien, mec, on t'a payé un salaire." Et ils auraient raison !
J’adore les analogies, mais on me dit souvent que ce n’est pas mon point fort. C'est comme si vous étiez un poisson qui aimait nager dans l'océan, mais vous êtes un poisson d'eau douce.
Et puis - j'ai décidé de faire une preuve de concept (POC), pendant mon temps libre, pour ne pas me tromper en proposant des idées qui ne seraient peut-être même pas réalisables.
Le plan initial était d'utiliser une bibliothèque prête à l'emploi existante pour le rendu des démarques, mais d'étendre ses capacités afin qu'elle puisse restituer non seulement les éléments standard de la liste des démarques, mais également quelque chose de beaucoup plus complexe. Les articles n’étaient pas seulement du texte avec des images. Il y avait aussi un beau design visuel, des lecteurs audio intégrés et bien plus encore.
J'ai passé 40 heures, du vendredi soir au lundi matin, pour tester cette hypothèse - à quel point cette bibliothèque est extensible pour de nouvelles fonctionnalités, à quel point tout fonctionne en général et, surtout, si cette solution peut renverser le moteur notoire du trône. L'hypothèse a été confirmée - après avoir désassemblé la bibliothèque jusqu'au bout et quelques correctifs, il est devenu possible d'enregistrer n'importe quel élément de l'interface utilisateur par des mots-clés ou des constructions de syntaxe spéciales, tout cela pourrait être facilement étendu et, plus important encore, cela pourrait vraiment remplacer le moteur d'article. . Je suis arrivé quelque part dans 15 heures. Les 25 restants, j’ai passé à finaliser le POC.
L'idée n'était pas seulement de remplacer un moteur par un autre - non. L’idée était de remplacer tout le processus ! Le panneau d'administration vous permet non seulement de créer des articles mais gère également le contenu visible dans l'application. L'idée originale était de créer un remplacement complet qui ne serait pas lié à un projet spécifique mais permettrait de le gérer. Plus important encore, ce remplacement devrait également fournir un éditeur pratique pour ces mêmes articles afin qu'ils puissent être créés et voir immédiatement le résultat.
Pour le POC, j'ai pensé qu'il suffirait de créer un éditeur. Cela ressemblait à ceci :
Après 40 heures, j'avais un éditeur de code fonctionnel composé d'un mélange turbulent de démarques et d'un tas de balises XML personnalisées (par exemple, <container>
), un aperçu affichant l'interface utilisateur de ce code en temps réel, et aussi le plus grand des poches sous les yeux que ce monde n'a jamais vues. Il convient également de noter que "l'éditeur de code" utilisé est une autre bibliothèque capable de mettre en évidence la syntaxe, mais le problème est qu'il peut mettre en évidence le markdown, il peut également mettre en évidence XML, mais la mise en évidence d'un méli-mélo se brise constamment. Ainsi, pendant les 40 heures, vous pouvez en ajouter quelques autres pour le codage singe d'une chimère qui permettra de mettre en évidence les deux dans une seule bouteille. Il est temps de se demander : que s'est-il passé ensuite ?
Vient ensuite la démo. J'ai réuni quelques cadres supérieurs, je leur ai expliqué ma vision pour résoudre le problème, le fait que j'ai confirmé cette vision dans la pratique, et j'ai montré ce qui fonctionne, comment et quelles sont ses possibilités.
Les gars ont aimé le travail. Et il y avait une envie de l'utiliser. Mais il y avait aussi une cupidité tenace. Ma cupidité. Ne pourrais-je pas le donner à l'entreprise comme ça ? Bien sûr que non. Mais je n’avais pas prévu de le faire non plus. La démo faisait partie d'un plan audacieux où je les ai choqués avec mon métier, ils ne pouvaient tout simplement pas résister et étaient prêts à remplir toutes les conditions, juste pour utiliser ce développement incroyable, exclusif et étonnant. Je ne divulguerai pas tous les détails de cette histoire fictive (!) , mais je dirai seulement que je voulais de l'argent. De l'argent et des vacances. Un mois de vacances payées, ainsi que de l'argent. Le montant d’argent n’est pas si important, il est seulement important que le montant soit en corrélation avec mon salaire et le chiffre 6.
Mais je n’étais pas un casse-cou complètement téméraire.
Dormammu, je suis venu négocier. Et l'accord était le suivant - je travaille deux semaines complètes dans mon mode ( dormir 4 heures, travailler 20 heures ), en terminant le POC à l'état "peut être utilisé aux fins de notre application", et en parallèle, je mets en œuvre une nouvelle fonctionnalité dans l'application - un écran entier, utilisant cet ultra-truc (pour lequel ces deux semaines étaient initialement prévues). Et au bout de deux semaines, nous organisons une autre manifestation. Seulement cette fois, nous rassemblons plus de personnes, même la haute direction de l'entreprise, et si ce qu'ils voient les impressionne et qu'ils veulent l'utiliser, l'accord est conclu, je réalise mes désirs et l'entreprise obtient un super pistolet. S'ils ne veulent pas de tout cela, je suis prêt à accepter le fait que j'ai travaillé gratuitement ces deux semaines.
Eh bien, le voyage à Urubici , que j'avais déjà prévu pour mes vacances d'un mois, n'a malheureusement jamais eu lieu. Les gars du manager n'ont pas osé accepter une telle audace. Et moi, baissant le regard vers le sol, je suis allé tailler un nouvel écran à la « manière classique ». Mais il n'existe pas d'histoire dans laquelle le personnage principal, vaincu par le destin, ne se relève pas et tente à nouveau d'apprivoiser sa bête.
Bien que non... il semble qu'il y en ait : 1 , 2 , 3 , 4 , 5 .
Après avoir regardé tous ces films, j'ai décidé que c'était un signe ! Et c'est encore mieux ainsi - c'est dommage de vendre un développement aussi prometteur pour quelques goodies là-bas ( de qui je plaisante ??? ), et je continuerai à développer mon projet davantage. Et j'ai continué. Mais plus 40 heures le week-end, seulement 15 à 20 heures par semaine, à un rythme relativement calme.
Briser le 4ème mur n’est pas une tâche facile. Tout comme essayer de trouver des titres intéressants qui inciteront le lecteur à continuer sa lecture et à attendre la fin de l'histoire de l'entreprise. Je terminerai l'histoire dans le deuxième article. Et maintenant, semble-t-il, il est temps de passer à l'implémentation, aux capacités fonctionnelles, et tout ça, ce qui, en théorie, devrait rendre cet article plus technique et HackerNoon !
La première chose dont nous parlerons est la syntaxe. L'idée originale du méli-mélo convenait au POC, mais comme la pratique l'a montré, la démarque n'est pas si simple. De plus, combiner certains éléments de démarque natifs avec des éléments purement Flutter n'est pas toujours cohérent.
La toute première question est : l'image sera-t-elle ![Description](Link)
ou <image>
?
Si c'est le premier, où dois-je placer un tas de paramètres ?
Si le second – pourquoi, alors, avons-nous le premier ?
La deuxième question concerne les textes. Les possibilités de Flutter pour styliser les textes sont illimitées. Les possibilités de démarque sont « couci-couça ». Oui, vous pouvez marquer le texte en gras ou en italique, et on a même pensé à utiliser ces constructions **
/ __
pour le style. Ensuite, on a pensé à placer les balises <color="red">
text </color>
au milieu, mais c'est une telle courbe et un tel fluage que le sang coule des yeux. Obtenir une sorte de HTML, avec sa propre syntaxe marginale, n'était pas du tout souhaitable. De plus, l’idée était que ce code pouvait être écrit même par des managers sans connaissances techniques.
Étape par étape, j'ai retiré la partie de la chimère et j'ai obtenu un super-mutant markdown. Autrement dit, nous avons une bibliothèque corrigée pour le rendu des démarques, mais remplie de balises personnalisées et sans prise en charge des démarques. C'est comme si nous avions du XML.
Je me suis assis pour réfléchir et expérimenter les autres syntaxes simples qui existent. JSON est un laitier. Faire écrire du JSON à une personne dans un éditeur Flutter tordu, c'est devenir un maniaque qui voudra vous tuer. Et ce n'est pas seulement ça, il ne me semble pas que JSON soit adapté à la saisie par une personne en général, en particulier pour l'interface utilisateur - il grandit constamment vers la droite, un tas de ""
obligatoires, il n'y a pas de commentaires. YAML ? Eh bien, peut-être. Mais le code explorera également latéralement. Il existe des liens intéressants, mais vous ne pouvez pas accomplir grand-chose avec leur seule aide. TOML ? Pf-ff.
D'accord, j'ai finalement opté pour XML. Il m'a semblé, et me semble encore aujourd'hui, qu'il s'agit d'une syntaxe plutôt "dense", très bien adaptée à l'interface utilisateur. Après tout, les concepteurs de mise en page HTML existent toujours, et ici tout sera encore plus simple que sur le Web ( probablement ).
Ensuite, la question s'est posée : ce serait bien d'avoir la possibilité de mettre en évidence/de compléter le code. Ainsi que des constructions logiques, un peu {{ user.name }}
. Ensuite, j'ai commencé à expérimenter avec Twig, Liquid, j'ai regardé d'autres moteurs de modèles dont je ne me souviens plus. Mais j'ai rencontré un autre problème : il est tout à fait possible de mettre en œuvre une partie de ce qui était prévu sur un moteur standard, par exemple Twig, mais cela ne fonctionnera certainement pas de tout mettre en œuvre. Et oui, c'est bien qu'il y ait une saisie semi-automatique et une mise en surbrillance, mais elles n'interféreront que si vous ajoutez vos propres nouvelles fonctionnalités au-dessus de la syntaxe standard de Twig, qui sera nécessaire pour Flutter. Du coup, avec XML, tout s'est très bien passé, les expérimentations avec Twig/Liquid n'ont pas donné de résultats exceptionnels, et à certains moments, je me suis même heurté à l'impossibilité d'implémenter certaines fonctionnalités. Par conséquent, le choix restait toujours avec XML. Nous parlerons davantage des fonctionnalités, mais pour l'instant, concentrons-nous sur l'auto-complétion et la mise en évidence, qui étaient si tentantes dans Twig/Liquid.
La prochaine chose que je veux dire, c'est que Flutter a des entrées de texte tordues. Ils fonctionnent bien au format mobile. Également bon au format de bureau lorsqu'il s'agit de quelque chose, enfin, un maximum de 5 à 10 lignes de hauteur. Mais lorsqu'il s'agit d'un éditeur de code à part entière, où cet éditeur est implémenté dans Flutter, vous ne pouvez pas le regarder sans larmes. Dans Trello , où je garde une trace de toutes les tâches et où j'écris des notes et des idées, il existe une telle "tâche" :
En fait, presque dès le début du travail sur le projet, j'ai gardé à l'esprit l'idée de remplacer l'éditeur de code Nui par quelque chose de plus adéquat. Disons : intégrez une vue Web avec la partie Open Source de VS Code. Mais jusqu'à présent, mes mains n'y sont pas parvenues, d'ailleurs, une solution fragile mais toujours efficace au problème de courbure de cet éditeur m'est venue à l'esprit : utiliser votre propre environnement de développement à la place.
Ceci est réalisé comme suit - créez un fichier avec le code UI (XML), idéalement avec l'extension .html
/ .twig
, ouvrez le même fichier via le CMS - Web / Desktop / Local / Deployed - cela n'a pas d'importance. Et ouvrez le même fichier via n'importe quel IDE, même via la version Web de VS Code. Et voilà : vous pouvez modifier ce fichier dans votre outil préféré et avoir un aperçu en temps réel directement dans le navigateur ou n'importe où.
Dans un tel scénario, vous pouvez même viser l'auto-complétion à part entière. Dans VS Code, il existe la possibilité de l'implémenter via des balises HTML personnalisées. Cependant, je n'utilise pas VS Code, mon choix se porte sur IntelliJ IDEA et pour cet IDE il n'y a plus de solution aussi simple (enfin, du moins il n'y en avait pas, ou du moins je ne l'ai pas trouvée). Mais il existe une solution plus générale qui fonctionnera à la fois ici et là : la définition de schéma XML (XSD). J'ai passé environ 3 soirées à essayer de comprendre ce monstre, mais le succès n'est jamais venu et, à la fin, j'ai abandonné cette affaire, la laissant pour des temps meilleurs.
Il est également intéressant qu'au final, après de nombreuses itérations d'expérimentations, de mises à jour, disons, du moteur chargé de convertir XML en widgets, nous ayons obtenu une telle solution pour laquelle le langage n'est pas particulièrement important. Tout comme le support d'informations sur la structure de votre interface utilisateur, le choix s'est finalement porté sur XML, mais en même temps, vous pouvez le nourrir en toute sécurité avec du JSON, et même une forme binaire - compilée avec Protobuf. Et cela nous amène au sujet suivant.
Dans cette phrase, la taille de cet article sera de 3218 mots. Quand j'ai commencé à écrire cette section, pour tout faire de manière qualitative, il était nécessaire d'écrire de nombreux cas de test comparant les performances du rendu Nui et de Flutter standard. Puisque j'avais déjà implémenté un écran de démonstration, entièrement créé sur Nui :
il fallait créer nativement une correspondance exacte de l’écran (dans le contexte de Flutter, bien sûr). En conséquence, cela a pris plus de 3 semaines, beaucoup de réécriture de la même chose, d'amélioration du processus de test et d'obtention de chiffres de plus en plus intéressants. Et la taille de cette section à elle seule dépassait les 3 500 mots. Par conséquent, j'en suis venu à l'idée qu'il était logique d'écrire un article séparé qui serait entièrement consacré aux performances de Nui, en tant que cas particulier, et au prix supplémentaire que vous devrez payer si vous décidez d'utiliser Nui. L'interface utilisateur pilotée par le serveur comme approche.
Mais je vais faire un petit spoiler : j'ai envisagé deux scénarios principaux pour évaluer les performances : le moment du rendu initial . C'est important si vous décidez d'implémenter l'intégralité de l'écran sur l'interface utilisateur pilotée par le serveur, et cet écran s'ouvrira quelque part dans votre application.
Donc si cet écran est très lourd, alors même un écran Flutter natif mettra beaucoup de temps à restituer, donc lors du passage à un tel écran, surtout si cette transition est accompagnée d'une animation, des décalages seront visibles. Le deuxième scénario est le frame time (FPS) avec des modifications dynamiques de l'interface utilisateur . Les données ont changé - vous devez redessiner un composant. La question est de savoir dans quelle mesure cela affectera le temps de rendu, si cela affectera tellement que lorsque l'écran est mis à jour, l'utilisateur verra des décalages. Et voici un autre spoiler : dans la plupart des cas, vous ne pourrez pas dire que l'écran que vous voyez est complètement implémenté sur Nui. Si vous intégrez un widget Nui dans un écran Flutter natif standard (par exemple, une zone de l'écran qui devrait changer de manière très dynamique dans l'application), vous êtes assuré de ne pas pouvoir le reconnaître. Il y a bien sûr des baisses de performances. Mais ils sont tels qu'ils n'affectent pas le FPS même à une fréquence d'images de 120 FPS - c'est-à-dire que le temps d'une image ne dépassera presque jamais 8ms
. Cela est vrai pour le deuxième scénario. Quant au premier, tout dépend du niveau de complexité de l'écran. Mais même ici, la différence sera telle qu'elle n'affectera pas la perception et ne fera pas de votre application une référence pour les utilisateurs de smartphones .
Vous trouverez ci-dessous trois enregistrements d'écran du Pixel 7a (Tensor G2, le taux de rafraîchissement de l'écran a été réglé sur 90 images (maximum pour cet appareil), taux d'enregistrement vidéo de 60 images par seconde (maximum pour les paramètres d'enregistrement). Toutes les 500 ms, la position des éléments dans la liste est aléatoire, à partir des données dont les 3 premières cartes sont construites, et après 500 ms supplémentaires, le statut de la commande passe au suivant. Serez-vous capable de deviner lequel de ces écrans est entièrement implémenté sur Nui ?
PS Le temps de chargement des images ne dépend pas de l'implémentation puisque sur cet écran, quelle que soit l'implémentation, il y a beaucoup d'images Svg - toutes des icônes, ainsi que des logos de marque. Tous les fichiers SVG (ainsi que les images normales) sont stockés sur GitHub, en tant qu'hébergement, afin qu'ils puissent se charger assez lentement, ce qui est observé dans certaines expériences.
Youtube:
Lors de la création de Nui, j'ai adhéré au concept suivant : il est nécessaire de créer un outil tel que, tout d'abord, les développeurs Flutter le trouveront aussi simple à utiliser que la création d'applications Flutter classiques. Par conséquent, l'approche pour nommer tous les composants était simple : les nommer de la même manière qu'ils sont nommés dans Flutter.
La même chose s'applique aux paramètres des widgets - les scalaires, comme String
, int
, double
, enum
, etc., qui, en tant que paramètre, ne sont pas configurés eux-mêmes. Ces types de paramètres dans Nui sont appelés arguments . Et aux paramètres de classe complexes, comme decoration
dans le widget Container
, appelés propriété . Cette règle n'est pas absolue car certaines propriétés sont trop verbeuses, leurs noms ont donc été simplifiés. Aussi, pour certains widgets, la liste des paramètres disponibles a été étendue. Par exemple, pour créer un SizedBox
ou Container
carré, vous ne pouvez transmettre qu'un seul argument personnalisé size
, au lieu de deux width
+ height
identiques.
Je ne donnerai pas une liste complète des widgets implémentés, car ils sont assez nombreux (53 pour le moment). En bref, vous pouvez implémenter presque toutes les interfaces utilisateur pour lesquelles il serait logique d'utiliser l'interface utilisateur pilotée par le serveur comme approche de principe. Y compris les effets de défilement complexes associés aux Slivers
.
De plus, concernant les composants, il convient de noter le point d'entrée ou le widget auquel vous devrez transmettre le code XML du cloud. Il existe actuellement deux widgets de ce type : NuiListWidget
et NuiStackWidget
.
Le premier, de par sa conception, doit être utilisé si vous devez implémenter l’intégralité de l’écran. Sous le capot, il s'agit d'un CustomScrollView
contenant tous les widgets qui seront analysés à partir du code de balisage d'origine. De plus, l'analyse, pourrait-on dire, est "intelligente": puisque le contenu de CustomScrollView
devrait être slivers
, alors une solution possible serait d'envelopper chacun des widgets du flux dans un SliverToBoxAdapter
, mais cela aurait un impact extrêmement négatif sur les performances. Par conséquent, les widgets sont intégrés dans leur parent comme suit - en commençant par le tout premier, nous parcourons la liste jusqu'à ce que nous rencontrions un vrai sliver
. Dès que nous rencontrons un sliver
, nous ajoutons tous les widgets précédents à SliverList
et l'ajoutons au CustomScrollView
parent. Ainsi, les performances de rendu de l'ensemble de l'interface utilisateur seront aussi élevées que possible, car le nombre de slivers
sera minime. Pourquoi est-il mauvais d'avoir beaucoup de slivers
dans CustomScrollView
? La réponse est ici .
Le deuxième widget - NuiStackWidget
peut également être utilisé en plein écran - dans ce cas, il convient de garder à l'esprit que tout ce que vous créez sera intégré dans la Stack
dans le même ordre. Et il sera également nécessaire d'utiliser explicitement slivers
- c'est-à-dire que si vous voulez une liste de slivers
- vous devrez ajouter CustomScrollView
et déjà implémenter la liste à l'intérieur.
Le deuxième scénario est l’implémentation d’un petit widget pouvant être intégré dans des composants natifs. Disons - de réaliser une fiche produit qui sera entièrement personnalisable à l'initiative du serveur. Cela semble un scénario très intéressant dans lequel vous pouvez implémenter tous les composants de la bibliothèque de composants à l'aide de Nui et les utiliser comme widgets classiques. Dans le même temps, il sera toujours possible de les modifier complètement sans mettre à jour l'application.
Il convient de noter que NuiListWidget
peut également être utilisé comme un widget local, et non comme un écran entier, mais pour ce widget, vous devrez appliquer des restrictions appropriées, telles que la définition d'une hauteur explicite pour le widget parent.
Voici à quoi ressemblerait une counter app
si elle était créée à l'aide de 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), ), ); } }
Et voici un autre exemple, uniquement entièrement sur Nui (y compris la logique) :
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: {}, ), ), ); } }
Séparez le code de l'interface utilisateur pour qu'il soit mis en surbrillance :
<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>
Il existe également une documentation interactive et complète qui affiche des informations détaillées sur les arguments et les propriétés de chaque widget, ainsi que toutes leurs valeurs possibles. Pour chacune des propriétés, qui peuvent également avoir à la fois des arguments et d'autres propriétés, il existe également une documentation, avec une démonstration complète de toutes les valeurs disponibles. En plus de cela, chacun des composants contient un exemple interactif dans lequel vous pouvez voir l'implémentation de ce widget en direct et jouer avec en le modifiant à votre guise.
Nui est très étroitement intégré au CMS Nanc. Vous n'êtes pas obligé d'utiliser Nanc pour utiliser Nui, mais utiliser Nanc peut vous apporter des avantages, à savoir - la même documentation interactive, ainsi que Playground, où vous pouvez voir les résultats de la mise en page en temps réel, jouer avec les données qui y sera utilisé. De plus, il n'est pas nécessaire de créer votre propre version locale du CMS, vous pouvez tout à fait vous débrouiller avec la démo publiée, dans laquelle vous pouvez faire tout ce dont vous avez besoin.
Vous pouvez le faire en suivant le lien , puis en cliquant sur le champ Page Interface / Screen
. L'écran ouvert peut être utilisé comme terrain de jeu, et en cliquant sur le bouton Sync , vous pouvez synchroniser Nanc avec votre IDE via un fichier avec les sources, et toute la documentation est disponible en cliquant sur le bouton Aide .
PS Ces complexités existent parce que je n'ai jamais trouvé le temps de créer une page séparée explicite avec de la documentation sur les composants de Nanc, ainsi que par l'impossibilité d'insérer un lien direct vers cette page.
Il serait trop inutile de créer un mappeur ordinaire de XML vers des widgets. Bien sûr, cela peut aussi être utile, mais il y aura beaucoup moins de cas d'utilisation. Ce n'est pas la même chose - des composants et des écrans entièrement interactifs avec lesquels vous pouvez interagir, que vous pouvez mettre à jour de manière granulaire (c'est-à-dire pas tous en même temps - mais par parties qui nécessitent une mise à jour). De plus, cette interface utilisateur a besoin de données. Ce qui, compte tenu de la présence de la lettre S dans l'expression Server-Driven UI, peut être remplacé directement dans la mise en page sur le serveur, mais vous pouvez également le faire de manière plus esthétique. Et ne pas faire glisser une nouvelle partie de la mise en page depuis le backend pour chaque changement dans l'interface utilisateur (Nui n'est pas une machine à voyager dans le temps qui fait flotter les meilleures pratiques de jQuery).
Commençons par la logique : des variables et des expressions calculées peuvent être substituées dans la mise en page. Disons qu'un widget est défini comme <container color="{{ page.background }}">
extraira sa couleur directement des données transmises au "contexte parent" stocké dans la variable background
. Et <aspectRatio ratio="{{ 3 / 4}}">
définira la valeur du rapport hauteur/largeur correspondante pour ses descendants. Il existe des fonctions intégrées, des comparaisons et bien plus encore qui peuvent être utilisées pour créer une interface utilisateur avec une certaine logique.
Le deuxième point concerne les modèles . Vous pouvez définir votre propre widget directement dans le code de l'interface utilisateur à l'aide de la balise <template id="your_component_name"/>
. Dans le même temps, tous les composants internes de ce modèle auront accès aux arguments passés à ce modèle, ce qui permettra un paramétrage flexible des composants personnalisés puis de les réutiliser à l'aide de la balise <component id="your_component_name"/>
. À l'intérieur des modèles, vous pouvez transmettre non seulement des attributs mais également d'autres balises/widgets, ce qui permet de créer des composants réutilisables de toute complexité.
Troisième point - "pour les boucles". Dans Nui, il existe une balise <for>
intégrée qui vous permet d'utiliser des itérations pour restituer plusieurs fois le même (ou plusieurs) composants. Ceci est pratique lorsqu'il existe un ensemble de données à partir duquel vous devez créer une liste/ligne/colonne de widgets.
Quatrièmement - rendu conditionnel. Au niveau de la mise en page, la balise <show>
est implémentée (il y avait une idée de l'appeler <if>
), ce qui vous permet de dessiner des composants imbriqués, ou de ne pas les intégrer du tout dans l'arborescence sous diverses conditions.
Cinquième point : les actions. Certains composants avec lesquels l'utilisateur peut interagir peuvent envoyer des événements . Que vous pouvez contrôler complètement à votre guise. Disons, <inkWell onPressed="something">
- avec une telle déclaration, ce widget devient cliquable et votre application, ou plutôt un EventHandler
, pourra gérer cet événement et faire quelque chose. L’idée est que tout ce qui concerne la logique doit être implémenté directement dans l’application, mais vous pouvez tout implémenter. Créez des gestionnaires génériques capables de gérer des groupes d'actions, comme "aller à l'écran" / "appeler la méthode" / "envoyer un événement d'analyse". Il est également prévu d'implémenter du code dynamique, mais il y a ici des nuances. Pour Dart, il existe des moyens d'exécuter du code arbitraire, mais cela affecte les performances, et de plus, l'interopérabilité de ce code avec le code de l'application est à peine de 100 %. Autrement dit, en créant une logique dans ce code dynamique, vous rencontrerez constamment certaines limitations. Ce mécanisme doit donc être élaboré avec beaucoup de soin afin d’être réellement applicable et utile.
Le sixième point est la mise à jour de l'interface utilisateur locale. Ceci est possible grâce à la balise <dataBuilder>
. Cette balise (Bloc sous le capot) peut "regarder" un champ spécifique, et lorsqu'elle change, elle redessine son sous-arbre.
Au départ, j'ai suivi le chemin de deux magasins de données - le "contexte parent" mentionné ci-dessus. Ainsi que "data" - données qui peuvent être définies directement dans l'interface utilisateur, à l'aide de la balise <data>
. Pour être honnête, je ne me souviens plus de l'argumentation pour laquelle il était nécessaire de mettre en œuvre deux méthodes de stockage et de transfert de données vers l'interface utilisateur, mais je ne peux pas me critiquer sévèrement pour une telle décision.
Ils fonctionnent comme suit - le "contexte parent" est un objet de type Map<String, dynamic>
, passé directement aux widgets NuiListWidget
/ NuiStackWidget
. L'accès à ces données est possible par la page
préfixe :
<someWidget value="{{ page.your.field }}"/>
Vous pouvez faire référence à n'importe quoi, à n'importe quelle profondeur, y compris aux tableaux - {{ page.some.array.0.users.35.age }}
. S'il n'existe pas de clé/valeur de ce type, vous obtiendrez null
. Les listes peuvent être itérées en utilisant <for>
.
La deuxième façon - "données" est un magasin de données global. En pratique, il s'agit d'un certain Bloc
situé plus haut dans l'arborescence que NuiListWidget
/ NuiStackWidget
. En même temps, rien n'empêche d'organiser leur utilisation dans un style local, en passant votre propre instance de DataStorage
via DataStorageProvider
.
Dans le même temps, la première méthode n’est pas réactive : lorsque les données de page
changent, aucune interface utilisateur ne se met à jour. Puisqu’il ne s’agit en fait que des arguments de votre StatelessWidget
. Si la source de données pour page
est, disons, votre propre Bloc, qui donnera un ensemble de valeurs à Nui...Widget
- alors, comme avec un StatelessWidget
normal, il sera complètement redessiné avec de nouvelles données.
La deuxième façon de travailler avec les données est réactive. Si vous modifiez les données dans DataStorage
, en utilisant l'API de cette classe - la méthode updateValue
, alors cela appellera la méthode emit
de la classe Bloc, et s'il y a des auditeurs actifs de ces données dans votre UI - balises <dataBuilder>
, alors leur contenu sera modifié en conséquence, mais le reste de l'interface utilisateur ne sera pas touché.
Ainsi, nous obtenons deux sources de données potentielles : une page
très simple et une data
réactive. Hormis la logique de mise à jour des données dans ces sources et la réaction de l'interface utilisateur à ces mises à jour, il n'y a aucune différence entre elles.
Je n'ai délibérément pas décrit toutes les nuances et tous les aspects du travail, car il s'agirait d'une copie de la documentation déjà existante. Par conséquent, si vous souhaitez essayer ou simplement en apprendre davantage, bienvenue ici. Si certains aspects du travail ne sont pas clairs ou si la documentation ne couvre pas quelque chose, alors je serai flatté par votre message indiquant le problème :
Je vais brièvement énumérer certaines des fonctionnalités qui ne sont pas abordées dans cet article, mais qui sont à votre disposition :
Création de vos propres balises/composants, avec la possibilité de créer exactement la même documentation interactive pour eux, tout comme pour leurs arguments et propriétés avec aperçu en direct. C'est ainsi, par exemple, qu'est implémenté le composant de rendu des images SVG . Il ne sert à rien de le placer au cœur du moteur, car tout le monde n'en a pas besoin, mais en tant qu'extension disponible pour une utilisation en passant une seule variable - facile et simple. Directement -un exemple de mise en œuvre .
Une énorme bibliothèque intégrée d'icônes qui peut être étendue en ajoutant les vôtres (ici, je me suis avéré incohérent et "poussé", la logique était de rendre autant d'icônes que possible disponibles pour une utilisation immédiate et il n'était pas nécessaire de mettre à jour l'application pour utiliser de nouvelles icônes). Des versions prêtes à l'emploi sont disponibles : fluentui_system_icons , materials_design_icons_flutter et remixicon . Vous pouvez afficher toutes les icônes disponibles en utilisant Nanc , Page Interface / Screen -> Icons
Polices personnalisées, y compris Google Fonts prêtes à l'emploi
Conversion de XML en JSON/protobuf et utilisation de ces derniers comme « sources » pour l'interface utilisateur
Tout cela et bien plus encore peut être étudié dans la documentation .
L'essentiel est d'étudier la possibilité d'exécuter dynamiquement du code avec une logique. C'est une fonctionnalité très intéressante qui vous permettra d'étendre très sérieusement les capacités de Nui. En outre, vous pouvez (et devez) ajouter les widgets restants, rarement utilisés, mais parfois très importants, de la bibliothèque Flutter standard. Maîtriser XSD, afin que l'auto-complétion de toutes les balises apparaisse dans l'EDI (il y a une idée de générer ce schéma directement à partir de la documentation des balises, il sera alors facile de le créer pour les widgets personnalisés et il sera toujours à jour -date, et il y a aussi une idée de créer un DSL généré dans Dart, qui pourra ensuite être converti en XML/Json/Protobuf). Eh bien, et une optimisation supplémentaire des performances - ce n'est pas mal pour le moment, très pas mal, mais cela peut être encore meilleur, encore plus proche du Flutter natif.
C'est tout ce que j'ai. Dans le prochain article, je parlerai en détail des performances de Nui, de la façon dont j'ai créé des cas de test, du nombre de dizaines de rakes que j'ai parcourus au cours de ce processus et des chiffres qui peuvent être obtenus dans quels scénarios.
Si vous souhaitez essayer Nui ou mieux le connaître, veuillez vous rendre au bureau de documentation . De plus, si ce n'est pas difficile, veuillez mettre une étoile sur GitHub et un like sur pub.dev - ce n'est pas difficile pour vous, mais pour moi, un rameur solitaire sur cet immense bateau - c'est incroyablement utile.