paint-brush
So erstellen Sie eine servergesteuerte UI-Engine für Fluttervon@alphamikle
1,228 Lesungen
1,228 Lesungen

So erstellen Sie eine servergesteuerte UI-Engine für Flutter

von Mike Alfa30m2024/06/13
Read on Terminal Reader

Zu lang; Lesen

Die Geschichte hinter der Entwicklung von Nui – einer servergesteuerten UI-Engine für Flutter, die Teil eines größeren Projekts ist – Backendless CMS Nanc.
featured image - So erstellen Sie eine servergesteuerte UI-Engine für Flutter
Mike Alfa HackerNoon profile picture
0-item
1-item

Hallo!

Heute zeige ich Ihnen, wie Sie eine supertolle Engine für servergesteuerte Benutzeroberflächen in Flutter erstellen, die ein integraler Bestandteil eines supertollen CMS ist (so positioniert es sein Entwickler, also ich). Sie können natürlich eine andere Meinung haben, und ich werde diese gerne in den Kommentaren diskutieren.


Dieser Artikel ist der erste von zwei (bereits drei) in der Reihe. In diesem werden wir uns direkt mit Nui befassen, und im nächsten – wie tief Nui in Nanc CMS integriert ist, und zwischen diesem und dem nächsten Artikel wird es einen weiteren mit einer großen Menge an Informationen zur Nui-Leistung geben.


In diesem Artikel finden Sie viele interessante Dinge über Server-Driven UI, Nui-Funktionen (Nanc Server-Driven UI), Projektverlauf, Eigeninteressen und Doctor Strange. Ach ja, es wird auch Links zu GitHub und pub.dev geben, also wenn es Ihnen gefällt und Sie nichts dagegen haben, 1-2 Minuten Ihrer Zeit zu investieren, würde ich mich über Ihren Stern und Ihr „Gefällt mir“ freuen.


Inhaltsverzeichnis

  1. Einleitung
  2. Gründe für die Entwicklung
  3. Konzeptioneller Beweiß
  4. Syntax
  5. IDE-Editor
  6. Leistung
  7. Komponenten und UI-Erstellung
  8. Spielplatz
  9. Interaktivität und Logik
  10. Datentransfer
  11. Dokumentation
  12. Zukunftspläne

Eine kleine Einführung

Ich habe bereits einen Artikel über Nanc geschrieben, aber seitdem ist mehr als ein Jahr vergangen und das Projekt hat in Bezug auf Fähigkeiten und „Vollständigkeit“ bedeutende Fortschritte gemacht und, was am wichtigsten ist, es wurde mit fertiger Dokumentation und unter der MIT-Lizenz veröffentlicht.

Also, was ist Nanc?

Es ist ein Allzweck-CMS, das sein Backend nicht mit sich schleppt. Gleichzeitig ist es nicht so etwas wie React Admin, wo man zum Erstellen von etwas Unmengen von Code schreiben muss.


Um Nanc nutzen zu können, müssen Sie lediglich:

  1. Beschreiben Sie die Datenstrukturen, die Sie über das CMS mit Dart DSL verwalten möchten
  2. Schreiben Sie eine API-Schicht, die die Kommunikation zwischen dem CMS und Ihrem Backend implementiert


Darüber hinaus kann der erste Schritt vollständig über die Schnittstelle des CMS selbst durchgeführt werden, d. h. Sie können Datenstrukturen über die Benutzeroberfläche verwalten. Der zweite Schritt kann übersprungen werden, wenn:

  1. Sie verwenden Firebase
  2. Oder Sie verwenden Supabase
  3. Oder Sie möchten herumexperimentieren und Nanc ausführen, ohne es an ein echtes Backend zu binden - mit einer lokalen Datenbank (derzeit wird diese Rolle von einer JSON-Datei oder LocalStorage übernommen)


In manchen Szenarien müssen Sie also keine einzige Codezeile schreiben, um ein CMS zur Verwaltung Ihrer Inhalte und Daten zu erhalten. In Zukunft wird die Zahl dieser Szenarien zunehmen, sagen wir mal – plus GraphQL und RestAPI. Wenn Sie Ideen haben, wofür sonst noch ein SDK implementiert werden könnte, freue ich mich über Ihre Vorschläge in den Kommentaren.


Nanc arbeitet mit Entitäten, auch bekannt als Modelle, die auf der Ebene der Datenspeicherung als Tabelle (SQL) oder Dokument (No-SQL) dargestellt werden können. Jede Entität hat Felder – eine Darstellung von Spalten aus SQL oder dieselben „Felder“ aus No-SQL.


Einer der möglichen Feldtypen ist der sogenannte „Screen“-Typ. Das heißt, dieser gesamte Artikel ist der Text eines einzigen Felds aus dem CMS. Gleichzeitig sieht es architektonisch so aus: Es gibt eine völlig separate Bibliothek ( eigentlich mehrere Bibliotheken ) , die zusammen die Server-Driven UI Engine namens Nui implementieren. Diese Funktionalität ist in das CMS integriert, auf das viele zusätzliche Funktionen aufgesetzt sind.


Damit schließe ich den direkt Nanc gewidmeten Einführungsteil ab und beginne mit der Geschichte über Nui.

Wie alles begann

Haftungsausschluss: Alle Zufälle sind zufällig. Diese Geschichte ist fiktiv. Ich habe sie geträumt.

Ich habe in einem großen Unternehmen an mehreren Anwendungen gleichzeitig gearbeitet. Sie waren sich größtenteils ähnlich, wiesen aber auch viele Unterschiede auf.

Was jedoch bei ihnen völlig identisch war, war das, was ich als Artikel-Engine bezeichnen kann. Es bestand aus mehreren (5-10-15, ich erinnere mich nicht mehr genau) tausend Zeilen ziemlich zerknitterten Codes, der JSON vom Backend verarbeitete. Diese JSONs mussten schließlich in eine Benutzeroberfläche oder vielmehr in einen Artikel umgewandelt werden, der in einer mobilen Anwendung gelesen werden konnte.


Die Artikel wurden über das Admin-Panel erstellt und bearbeitet, und das Hinzufügen neuer Elemente war unglaublich, extrem mühsam und langwierig. Als ich diesen Horror sah, beschloss ich, die erste Optimierung vorzuschlagen – den armen Content-Managern gnädig zu sein und für sie die Funktion zur Vorschau von Artikeln in Echtzeit direkt im Browser im Admin-Panel zu implementieren.


Gesagt, getan. Nach einiger Zeit drehte sich ein dürrer Teil der Anwendung im Admin-Panel, was Content-Managern viel Zeit bei der Vorschau von Änderungen ersparte. Wenn sie früher einen Deep Link erstellen und dann für jede Änderung den Dev-Build öffnen, diesem Link folgen, auf Downloads warten und dann alles wiederholen mussten, konnten sie jetzt einfach Artikel erstellen und sie sofort sehen.


Aber mein Gedanke blieb hier nicht stehen - ich war zu genervt von dieser Engine und von anderen Entwicklern, da man nicht feststellen konnte, ob sie etwas hinzufügen oder einfach den Augiasstall ausmisten mussten.


Wenn Letzteres der Fall war, war der Entwickler bei den Meetings immer gut gelaunt – obwohl der Geruch … den kann die Kamera nicht einfangen.

Trifft das Erstere zu, war der Entwickler oft krank, hat Erdbeben erlebt, einen kaputten Computer, Kopfschmerzen, Meteoriteneinschläge, eine Depression im Endstadium oder eine Überdosis Apathie.


Um die Funktionalität der Engine zu erweitern, mussten auch zahlreiche neue Felder zum Admin-Panel hinzugefügt werden, damit Content-Manager die neuen Funktionen nutzen konnten.


Als ich das alles sah, kam mir ein unglaublicher Gedanke: Warum nicht eine allgemeine Lösung für dieses Problem schaffen? Eine Lösung, die uns davon abhält, das Admin-Panel und die Anwendung für jedes neue Element ständig zu optimieren und zu erweitern. Eine Lösung, die das Problem ein für alle Mal lösen würde! Und hier kommt die...

Hinterhältiger, gieriger kleiner Plan

Ich dachte: „Ich kann dieses Problem lösen. Ich kann der Firma viele Zehntausende, wenn nicht Hunderttausende sparen. Aber die Idee ist für die Firma vielleicht zu wertvoll, um sie einfach zu verschenken .“


Mit Geschenk meine ich, dass das Verhältnis des potenziellen Werts für das Unternehmen um Größenordnungen von dem abweicht, was das Unternehmen mir in Form eines Gehalts zahlen wird. Es ist, als ob Sie in einem frühen Stadium bei einem Startup anfangen würden, aber für ein geringeres Gehalt als das, was Ihnen in einem großen Unternehmen angeboten wird, und ohne Anteile am Unternehmen. Und dann wird das Startup zu einem Einhorn, und sie sagen Ihnen: „Also, Kumpel, wir haben dir ein Gehalt gezahlt.“ Und sie hätten recht!


Ich liebe Analogien, aber mir wird oft gesagt, dass sie nicht meine Stärke sind. Es ist, als ob Sie ein Fisch wären, der gerne im Meer schwimmt, aber Sie sind ein Süßwasserfisch.


Und dann habe ich beschlossen, in meiner Freizeit einen Proof of Concept (POC) durchzuführen, um es nicht zu vermasseln und Ideen anzubieten, die möglicherweise nicht einmal umsetzbar sind.

Konzeptioneller Beweiß

Der ursprüngliche Plan bestand darin, eine vorhandene vorgefertigte Bibliothek zum Rendern von Markdown zu verwenden, deren Funktionen jedoch so zu erweitern, dass sie nicht nur die Standardelemente aus der Markdown-Liste, sondern auch etwas viel Komplexeres rendern konnte. Die Artikel bestanden nicht nur aus Text mit Bildern. Es gab auch ein schönes visuelles Design, integrierte Audioplayer und vieles mehr.


Ich habe 40 Stunden, von Freitagabend bis Montagmorgen, damit verbracht, diese Hypothese zu testen – wie erweiterbar diese Bibliothek für neue Funktionen ist, wie gut alles im Allgemeinen funktioniert und vor allem – ob diese Lösung die berüchtigte Engine vom Thron stoßen kann. Die Hypothese wurde bestätigt – nachdem die Bibliothek bis auf die Knochen zerlegt und ein wenig gepatcht worden war, wurde es möglich, beliebige UI-Elemente durch Schlüsselwörter oder spezielle Syntaxkonstruktionen zu registrieren, all dies konnte problemlos erweitert werden und vor allem – es konnte die Artikel-Engine wirklich ersetzen. Ich kam in etwa 15 Stunden dazu. Die restlichen 25 verbrachte ich mit der Fertigstellung des POC.


Die Idee war nicht, nur eine Engine durch eine andere zu ersetzen – nein. Die Idee war, den gesamten Prozess zu ersetzen! Das Admin-Panel ermöglicht Ihnen nicht nur das Erstellen von Artikeln, sondern verwaltet auch Inhalte, die in der Anwendung sichtbar sind. Die ursprüngliche Idee war, einen vollständigen Ersatz zu schaffen, der nicht an ein bestimmtes Projekt gebunden ist, sondern dessen Verwaltung ermöglicht. Am wichtigsten – dieser Ersatz sollte auch einen praktischen Editor für genau diese Artikel bieten, damit Sie sie erstellen und das Ergebnis sofort sehen können.


Für den POC dachte ich, es würde ausreichen, einfach einen Editor zu erstellen. Er sah ungefähr so aus:

Benutzeroberflächen-Editor

Nach 40 Stunden hatte ich einen funktionierenden Code-Editor, der aus einer turbulenten Mischung aus Markdown und einer Reihe benutzerdefinierter XML-Tags (z. B. <container> ) bestand, eine Vorschau, die die Benutzeroberfläche dieses Codes in Echtzeit anzeigte, und auch die größten Tränensäcke, die diese Welt je gesehen hat. Es ist auch erwähnenswert, dass der verwendete „Code-Editor“ eine andere Bibliothek ist, die Syntaxhervorhebung unterstützt, aber das Problem ist, dass sie Markdown hervorheben kann, sie kann auch XML hervorheben, aber die Hervorhebung eines Sammelsuriums bricht ständig ab. Also können Sie für die 40 Stunden noch ein paar mehr für das Monkey-Coding einer Chimäre hinzufügen, die die Hervorhebung von beidem in einer Flasche bietet. Es ist Zeit zu fragen – was ist als Nächstes passiert?


Erste Demo

Als nächstes folgte die Demo. Ich versammelte ein paar leitende Manager, erklärte ihnen meine Vision zur Lösung des Problems, die Tatsache, dass ich diese Vision in der Praxis bestätigte, und zeigte, was funktioniert und wie und welche Möglichkeiten es bietet.


Den Jungs gefiel die Arbeit. Und sie wollten sie nutzen. Aber da war auch eine nagende Gier. Meine Gier. Konnte ich es der Firma nicht einfach so geben? Natürlich nicht. Aber das hatte ich auch nicht vor. Die Demo war Teil eines gewagten Plans, bei dem ich sie mit meinem Handwerk schockierte, sie konnten einfach nicht widerstehen und waren bereit, alle Bedingungen zu erfüllen, nur um diese unglaubliche, exklusive und erstaunliche Entwicklung nutzen zu können. Ich werde nicht alle Details dieser fiktiven (!) Geschichte preisgeben, aber ich sage nur, dass ich Geld wollte. Geld und Urlaub. Einen bezahlten einmonatigen Urlaub und auch Geld. Wie viel Geld ist nicht so wichtig, wichtig ist nur, dass der Betrag mit meinem Gehalt und der Zahl 6 korreliert.

Aber ich war kein völlig draufgängerischer Draufgänger.


Dormammu, ich bin gekommen, um zu verhandeln. Und der Deal war folgender: Ich arbeite zwei volle Wochen in meinem Modus ( schlafe 4 Stunden, arbeite 20 Stunden ), beende den POC bis zu einem Zustand, in dem er „für die Zwecke unserer App verwendet werden kann“, und parallel dazu implementiere ich eine neue Funktion in die Anwendung – einen ganzen Bildschirm, mithilfe dieses Ultra-Dings (für das diese zwei Wochen ursprünglich vorgesehen waren). Und nach zwei Wochen halten wir eine weitere Demo ab. Nur dieses Mal versammeln wir mehr Leute, sogar die oberste Führungsebene des Unternehmens, und wenn das, was sie sehen, sie beeindruckt und sie es verwenden möchten, ist der Deal abgeschlossen, meine Wünsche werden erfüllt und das Unternehmen bekommt eine Superwaffe. Wenn sie nichts davon wollen, bin ich bereit zu akzeptieren, dass ich diese zwei Wochen umsonst gearbeitet habe.


Pedra Furada (in der Nähe von Urubici)

Nun, die Reise nach Urubici , die ich bereits für meinen einmonatigen Urlaub geplant hatte, fand leider nie statt. Die Manager trauten sich nicht, einer solchen Kühnheit zuzustimmen. Und ich senkte meinen Blick zu Boden und machte mich auf „klassische Weise“ daran, einen neuen Bildschirm zu schnitzen. Aber es gibt keine Geschichte, in der die vom Schicksal besiegte Hauptfigur nicht von den Knien aufsteht und versucht, ihr Biest erneut zu zähmen.


Obwohl nein... es scheint, es gibt: 1 , 2 , 3 , 4 , 5 .


Nachdem ich mir all diese Filme angesehen hatte, kam ich zu dem Schluss, dass dies ein Zeichen war! Und dass es so sogar noch besser ist – es ist schade, eine so vielversprechende Entwicklung für ein paar Goodies zu verkaufen ( wen will ich hier eigentlich veräppeln??? ), und ich werde mein Projekt weiter entwickeln. Und ich machte weiter. Aber nicht mehr 40 Stunden am Wochenende, sondern nur noch 15-20 Stunden pro Woche, in einem relativ ruhigen Tempo.

Coden oder nicht codieren?

Die vierte Wand zu durchbrechen ist keine leichte Aufgabe. Genauso wie der Versuch, interessante Schlagzeilen zu finden, die den Leser dazu bringen, weiterzulesen und darauf zu warten, wie die Geschichte mit dem Unternehmen endet. Ich werde die Geschichte im zweiten Artikel beenden. Und jetzt, so scheint es, ist es an der Zeit, zur Implementierung, den funktionalen Möglichkeiten und all dem überzugehen, was diesen Artikel theoretisch technisch und HackerNoon besser machen sollte!

Syntax

Als erstes werden wir über die Syntax sprechen. Die ursprüngliche Sammelsurium-Idee war für POC geeignet, aber wie die Praxis gezeigt hat, ist Markdown nicht so einfach. Außerdem ist die Kombination einiger nativer Markdown-Elemente mit reinen Flutter- Elementen nicht immer konsistent.


Die allererste Frage lautet: Wird das Bild ![Description](Link) oder <image> sein?


Wenn das erste - wo stecke ich einen Haufen Parameter hinein?

Wenn das Zweite zutrifft, warum haben wir dann das Erste?


Die zweite Frage betrifft Texte. Die Möglichkeiten von Flutter zum Stylen von Texten sind grenzenlos. Die Möglichkeiten von Markdown sind „so lala“. Ja, Sie können den Text fett oder kursiv markieren, und es gab sogar Überlegungen, diese Konstruktionen ** / __ zum Stylen zu verwenden. Dann gab es Überlegungen, <color="red"> Text-Tags </color> in die Mitte zu schieben, aber das ist so eine Kurve und ein solcher Kriecher, dass einem das Blut aus den Augen fließt. Eine Art HTML mit einer eigenen Randsyntax zu bekommen, war überhaupt nicht wünschenswert. Außerdem war die Idee, dass dieser Code sogar von Managern ohne technische Kenntnisse geschrieben werden könnte.


Schritt für Schritt entfernte ich den Teil der Chimäre und bekam einen Markdown-Supermutanten. Das heißt, wir bekamen eine gepatchte Bibliothek zum Rendern von Markdown, aber vollgestopft mit benutzerdefinierten Tags und ohne Markdown-Unterstützung. Das ist, als hätten wir XML.


Ich habe mich hingesetzt, um darüber nachzudenken und zu experimentieren, welche anderen einfachen Syntaxen es gibt. JSON ist Schlacke. Wenn man jemanden dazu bringt, JSON in einem korrupten Flutter-Editor zu schreiben, bekommt man einen Verrückten, der einen umbringen will. Und das ist nicht alles, ich glaube nicht, dass JSON für die menschliche Eingabe im Allgemeinen geeignet ist, insbesondere für die Benutzeroberfläche – es wächst ständig nach rechts, ein Haufen obligatorischer "" , es gibt keine Kommentare. YAML? Na ja, vielleicht. Aber der Code wird auch seitwärts kriechen. Es gibt interessante Links, aber allein mit ihrer Hilfe kann man nicht viel erreichen. TOML? Pf-ff.


Okay, ich habe mich schließlich doch für XML entschieden. Es schien mir und scheint mir auch heute noch, dass dies eine ziemlich „dichte“ Syntax ist, die sich sehr gut für die Benutzeroberfläche eignet. Schließlich gibt es immer noch HTML-Layout-Designer, und hier wird alles noch einfacher sein als im Web ( wahrscheinlich ).


Als nächstes stellte sich die Frage: Es wäre schön, die Möglichkeit einer Hervorhebung/Codevervollständigung zu haben. Sowie logische Konstruktionen, so etwas wie {{ user.name }} . Dann begann ich mit Twig und Liquid zu experimentieren und sah mir einige andere Template-Engines an, an die ich mich nicht mehr erinnere. Aber ich stieß auf ein anderes Problem: Es ist durchaus möglich, einen Teil dessen, was geplant war, auf einer Standard-Engine, sagen wir Twig, umzusetzen, aber es wird definitiv nicht funktionieren, alles umzusetzen. Und ja, es ist gut, dass es Autovervollständigung und Hervorhebung geben wird, aber sie werden nur stören, wenn Sie Ihre eigenen neuen Funktionen auf die Standard-Twig-Syntax aufsetzen, die für Flutter benötigt wird. Als Ergebnis lief mit XML alles sehr gut, Experimente mit Twig/Liquid brachten keine herausragenden Ergebnisse, und an bestimmten Stellen stieß ich sogar auf die Unmöglichkeit, einige Funktionen zu implementieren. Daher blieb die Wahl immer noch bei XML. Wir werden mehr über die Funktionen sprechen, aber konzentrieren wir uns vorerst auf Autovervollständigung und Hervorhebung, die in Twig/Liquid so verlockend waren.


IDE

Als nächstes möchte ich sagen, dass Flutter krumme Texteingaben hat. Sie funktionieren gut im Mobilformat. Auch gut im Desktopformat, wenn es um etwas geht, na ja, maximal 5-10 Zeilen hoch. Aber wenn es um einen vollwertigen Code-Editor geht, wo dieser Editor in Flutter implementiert ist, kann man ihn nicht ohne Tränen betrachten. In Trello , wo ich alle Aufgaben verfolge und Notizen und Ideen schreibe, gibt es eine solche „Aufgabe“:


Aufgabe zum Ändern des UI-Code-Editors


Tatsächlich hatte ich fast von Beginn der Arbeit an dem Projekt an die Idee, den Nui-Code-Editor durch etwas Passenderes zu ersetzen. Sagen wir, eine Webansicht mit dem Open-Source-Teil von VS Code einzubetten. Aber bisher habe ich das nicht geschafft, außerdem ist mir eine knifflige, aber immer noch funktionierende Lösung für das Problem der Krümmung dieses Editors in den Sinn gekommen – stattdessen eine eigene Entwicklungsumgebung zu verwenden.


Dies wird folgendermaßen erreicht: Erstellen Sie eine Datei mit UI-Code (XML), idealerweise mit der Erweiterung .html / .twig , öffnen Sie dieselbe Datei über das CMS – Web / Desktop / Lokal / Bereitgestellt – das spielt keine Rolle. Und öffnen Sie dieselbe Datei über eine beliebige IDE, sogar über die Webversion von VS Code. Und voilà – Sie können diese Datei in Ihrem bevorzugten Tool bearbeiten und eine Echtzeitvorschau direkt im Browser oder überall anzeigen.


Nanc + IDE-Synchronisierung


In einem solchen Szenario können Sie sogar eine vollwertige Autovervollständigung einbauen. In VS Code besteht die Möglichkeit, diese durch benutzerdefinierte HTML-Tags zu implementieren. Ich verwende jedoch nicht VS Code, sondern IntelliJ IDEA, und für diese IDE gibt es keine so einfache Lösung mehr (zumindest gab es sie nicht, oder zumindest habe ich sie nicht gefunden). Aber es gibt eine allgemeinere Lösung, die sowohl dort als auch dort funktioniert – XML Schema Definition (XSD). Ich habe ungefähr drei Abende damit verbracht, dieses Monster zu verstehen, aber der Erfolg kam nie, und am Ende habe ich diese Angelegenheit aufgegeben und sie für bessere Zeiten aufgehoben.


Interessant ist auch, dass wir am Ende, nach vielen Iterationen von Experimenten und Updates, sagen wir, der Engine, die für die Konvertierung von XML in Widgets zuständig ist, eine solche Lösung bekommen haben, für die die Sprache nicht besonders wichtig ist. Nur als Träger von Informationen über die Struktur Ihrer Benutzeroberfläche fiel die Wahl letztendlich auf XML, aber gleichzeitig können Sie es problemlos mit JSON oder sogar einer binären Form füttern – kompiliert mit Protobuf. Und das bringt uns zum nächsten Thema.


Leistung

In diesem Satz beträgt die Länge dieses Artikels 3218 Wörter. Als ich begann, diesen Abschnitt zu schreiben, musste ich, um alles qualitativ zu machen, viele Testfälle schreiben, in denen die Leistung des Renderings von Nui und normalem Flutter verglichen wurde. Da ich bereits einen Demo-Bildschirm implementiert hatte, der vollständig auf Nui erstellt wurde:


Nalmart-Bildschirmdemo


es war notwendig, eine exakte native Entsprechung des Bildschirms zu erstellen (natürlich im Kontext von Flutter). Infolgedessen dauerte es mehr als 3 Wochen, viel Umschreiben derselben Sache, Verbesserung des Testprozesses und Erhalt immer interessanterer Zahlen. Und allein dieser Abschnitt umfasste mehr als 3500 Wörter. Daher kam ich auf die Idee, dass es sinnvoll wäre, einen separaten Artikel zu schreiben, der sich ganz und gar der Leistung von Nui als Sonderfall und dem zusätzlichen Preis widmet, den Sie zahlen müssen, wenn Sie sich für Server-Driven UI als Ansatz entscheiden.


Aber ich verrate Ihnen etwas: Ich habe zwei Hauptszenarien zur Leistungsbewertung in Betracht gezogen – die Zeit des ersten Renderings . Dies ist wichtig, wenn Sie sich für die Implementierung des gesamten Bildschirms auf der servergesteuerten Benutzeroberfläche entscheiden und dieser Bildschirm irgendwo in Ihrer Anwendung geöffnet wird.


Wenn dieser Bildschirm also sehr schwer ist, dauert das Rendern selbst eines nativen Flutter-Bildschirms sehr lange. Wenn Sie also zu einem solchen Bildschirm wechseln, insbesondere wenn dieser Übergang von einer Animation begleitet wird, sind Verzögerungen sichtbar. Das zweite Szenario ist die Frame-Zeit (FPS) mit dynamischen UI-Änderungen . Die Daten haben sich geändert - Sie müssen einige Komponenten neu zeichnen. Die Frage ist, wie stark sich dies auf die Renderzeit auswirkt, ob es sich so stark auswirkt, dass der Benutzer beim Aktualisieren des Bildschirms Verzögerungen sieht. Und hier ist noch ein Spoiler - in den meisten Fällen können Sie nicht erkennen, dass der angezeigte Bildschirm vollständig auf Nui implementiert ist. Wenn Sie ein Nui-Widget in einen normalen, nativen Flutter-Bildschirm einbetten (z. B. einen Bereich des Bildschirms, der sich in der Anwendung sehr dynamisch ändern sollte), werden Sie dies garantiert nicht erkennen. Natürlich gibt es Leistungseinbußen. Sie sind jedoch so beschaffen, dass sie die FPS selbst bei einer Frame-Rate von 120 FPS nicht beeinträchtigen - das heißt, die Zeit eines Frames wird fast nie 8ms überschreiten . Dies gilt für das zweite Szenario. Was den ersten Punkt betrifft, hängt alles von der Komplexität des Bildschirms ab. Aber auch hier wird der Unterschied so groß sein, dass er die Wahrnehmung nicht beeinträchtigt und Ihre Anwendung nicht zum Maßstab für die Smartphones der Benutzer macht.


Unten sind drei Bildschirmaufnahmen von Pixel 7a (Tensor G2, Bildschirmaktualisierungsrate wurde auf 90 Bilder eingestellt (Maximum für dieses Gerät), Videoaufzeichnungsrate von 60 Bildern pro Sekunde (Maximum für Aufnahmeeinstellungen). Alle 500 ms wird die Position der Elemente in der Liste zufällig generiert, aus deren Daten die ersten 3 Karten erstellt werden, und nach weiteren 500 ms wird der Bestellstatus auf den nächsten umgeschaltet. Können Sie erraten, welcher dieser Bildschirme vollständig auf Nui implementiert ist?


PS: Die Ladezeit der Bilder hängt nicht von der Implementierung ab, da auf diesem Bildschirm bei jeder Implementierung viele SVG-Bilder vorhanden sind – alle Symbole sowie Markenlogos. Alle SVGs (sowie normale Bilder) werden auf GitHub als Hosting gespeichert, sodass sie recht langsam geladen werden können, was bei einigen Experimenten beobachtet wurde.


Youtube:


Verfügbare Komponenten - So erstellen Sie eine Benutzeroberfläche

Bei der Erstellung von Nui habe ich mich an das folgende Konzept gehalten: Es ist notwendig, ein solches Tool zu erstellen, das Flutter-Entwicklern vor allem die Verwendung so einfach macht wie das Erstellen regulärer Flutter-Anwendungen. Daher war der Ansatz zur Benennung aller Komponenten einfach: Sie wurden auf die gleiche Weise benannt, wie sie in Flutter benannt werden.


Dasselbe gilt für Widget-Parameter – die Skalare wie String , int , double , enum usw., die als Parameter selbst nicht konfiguriert werden. Diese Arten von Parametern werden innerhalb von Nui Argumente genannt. Und komplexe Klassenparameter, wie decoration im Container Widget, heißen Eigenschaft . Diese Regel ist nicht absolut, da einige Eigenschaften zu ausführlich sind und ihre Namen daher vereinfacht wurden. Zudem wurde für einige Widgets die Liste der verfügbaren Parameter erweitert. Um beispielsweise eine quadratische SizedBox oder Container zu erstellen, können Sie nur ein benutzerdefiniertes Argument size übergeben, statt zwei identische width + height .


Ich werde keine vollständige Liste der implementierten Widgets angeben, da es ziemlich viele davon gibt (derzeit 53). Kurz gesagt: Sie können praktisch jede Benutzeroberfläche implementieren, für die es grundsätzlich sinnvoll wäre, Server-Driven UI als Ansatz zu verwenden. Einschließlich komplexer Bildlaufeffekte im Zusammenhang mit Slivers .


Implementierte Widgets



In Bezug auf die Komponenten ist außerdem der Einstiegspunkt oder das Widget zu beachten, an das Sie den Cloud-XML-Code übergeben müssen. Derzeit gibt es zwei solcher Widgets - NuiListWidget und NuiStackWidget .


Die erste sollte konzeptgemäß verwendet werden, wenn Sie den gesamten Bildschirm implementieren müssen. Im Hintergrund handelt es sich um eine CustomScrollView , die alle Widgets enthält, die aus dem ursprünglichen Markup-Code analysiert werden. Darüber hinaus ist die Analyse, könnte man sagen, „intelligent“: Da der Inhalt von CustomScrollView aus slivers bestehen sollte, wäre eine mögliche Lösung, jedes der Widgets im Stream in einen SliverToBoxAdapter zu packen, was sich jedoch äußerst negativ auf die Leistung auswirken würde. Daher werden die Widgets wie folgt in ihr übergeordnetes Element eingebettet – beginnend mit dem allerersten gehen wir die Liste durch, bis wir auf ein echtes sliver stoßen. Sobald wir auf ein sliver stoßen, fügen wir alle vorherigen Widgets zu SliverList hinzu und fügen es dem übergeordneten CustomScrollView hinzu. Auf diese Weise ist die Leistung beim Rendern der gesamten Benutzeroberfläche so hoch wie möglich, da die Anzahl der slivers minimal ist. Warum ist es schlecht, viele slivers in CustomScrollView zu haben? Die Antwort finden Sie hier .


Das zweite Widget – NuiStackWidget – kann auch als Vollbild verwendet werden. In diesem Fall sollten Sie bedenken, dass alles, was Sie erstellen, in derselben Reihenfolge in den Stack eingebettet wird. Außerdem müssen Sie explizit slivers verwenden. Wenn Sie also eine Liste von slivers möchten, müssen Sie CustomScrollView hinzufügen und die Liste bereits darin implementieren.


Das zweite Szenario ist die Implementierung eines kleinen Widgets, das in native Komponenten eingebettet werden kann. Nehmen wir an, Sie möchten eine Produktkarte erstellen, die auf Initiative des Servers vollständig anpassbar ist. Dies scheint ein sehr interessantes Szenario zu sein, in dem Sie alle Komponenten in der Komponentenbibliothek mithilfe von Nui implementieren und als normale Widgets verwenden können. Gleichzeitig besteht immer die Möglichkeit, sie vollständig zu ändern, ohne die Anwendung zu aktualisieren.


Es ist erwähnenswert, dass NuiListWidget auch als lokales Widget und nicht für den gesamten Bildschirm verwendet werden kann. Für dieses Widget müssen Sie jedoch entsprechende Einschränkungen anwenden, z. B. das Festlegen einer expliziten Höhe für das übergeordnete Widget.


So würde eine counter app aussehen, wenn sie mit Flutter erstellt würde:

 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), ), ); } }


Und hier noch ein Beispiel, nur komplett auf Nui (inkl. Logik):

 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: {}, ), ), ); } }


Separater UI-Code, sodass eine Hervorhebung erfolgt:

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

Nui Counter App mit Nui-Logik


Es gibt auch eine interaktive und umfassende Dokumentation, die detaillierte Informationen darüber enthält, welche Argumente und Eigenschaften jedes Widget hat, sowie alle ihre möglichen Werte. Für jede der Eigenschaften, die sowohl Argumente als auch andere Eigenschaften haben können, gibt es auch eine Dokumentation mit einer vollständigen Demonstration aller verfügbaren Werte. Darüber hinaus enthält jede der Komponenten ein interaktives Beispiel, in dem Sie die Implementierung dieses Widgets live sehen und damit spielen können, indem Sie es nach Belieben ändern.

Nanc-Spielplatz

Nui ist sehr eng in Nanc CMS integriert. Sie müssen Nanc nicht verwenden, um Nui zu verwenden, aber die Verwendung von Nanc kann Ihnen Vorteile bieten, nämlich dieselbe interaktive Dokumentation sowie Playground, wo Sie die Ergebnisse des Layouts in Echtzeit sehen und mit den darin verwendeten Daten spielen können. Darüber hinaus ist es nicht erforderlich, einen eigenen lokalen Build des CMS zu erstellen. Sie können mit der veröffentlichten Demo, in der Sie alles tun können, was Sie benötigen, gut auskommen.


Sie können dies tun, indem Sie dem Link folgen und dann auf das Feld Page Interface / Screen klicken. Der geöffnete Bildschirm kann als Spielplatz verwendet werden. Durch Klicken auf die Schaltfläche „Synchronisieren “ können Sie Nanc über eine Datei mit Quellen mit Ihrer IDE synchronisieren. Die gesamte Dokumentation ist verfügbar, wenn Sie auf die Schaltfläche „Hilfe“ klicken.


PS: Diese Komplexität besteht, weil ich nie die Zeit fand, eine explizite separate Seite mit Dokumentation zu den Komponenten in Nanc zu erstellen, und es außerdem nicht möglich war, einen direkten Link zu dieser Seite einzufügen.


Interaktivität und Logik

Es wäre zu sinnlos, einen gewöhnlichen Mapper von XML zu Widgets zu erstellen. Das kann natürlich auch nützlich sein, aber es wird viel weniger Anwendungsfälle geben. Nicht dasselbe – vollständig interaktive Komponenten und Bildschirme, mit denen Sie interagieren können und die Sie granular aktualisieren können (das heißt, nicht alle auf einmal – sondern in Teilen, die aktualisiert werden müssen). Außerdem benötigt diese Benutzeroberfläche Daten. Die, unter Berücksichtigung des Buchstabens S in der Phrase Server-Driven UI, direkt in das Layout auf dem Server eingesetzt werden können, aber Sie können es auch schöner machen. Und nicht, um für jede Änderung in der Benutzeroberfläche einen neuen Teil des Layouts aus dem Backend zu ziehen (Nui ist keine Zeitmaschine, die die Best Practices von jQuery in Flutter überträgt).


Beginnen wir mit der Logik: Variablen und berechnete Ausdrücke können in das Layout eingesetzt werden. Angenommen, ein Widget ist definiert als <container color="{{ page.background }}"> extrahiert seine Farbe direkt aus den Daten, die an den in der background gespeicherten „übergeordneten Kontext“ übergeben werden. Und <aspectRatio ratio="{{ 3 / 4}}"> legt den entsprechenden Seitenverhältniswert für seine Nachkommen fest. Es gibt integrierte Funktionen, Vergleiche und vieles mehr, die zum Erstellen einer Benutzeroberfläche mit einer gewissen Logik verwendet werden können.


Der zweite Punkt ist die Templating-Funktion . Sie können Ihr eigenes Widget direkt im UI-Code mit dem Tag <template id="your_component_name"/> definieren. Gleichzeitig haben alle internen Komponenten dieser Vorlage Zugriff auf die an diese Vorlage übergebenen Argumente, was eine flexible Parametrisierung benutzerdefinierter Komponenten und deren anschließende Wiederverwendung mit dem Tag <component id="your_component_name"/> ermöglicht. Innerhalb von Vorlagen können Sie nicht nur Attribute, sondern auch andere Tags/Widgets übergeben, wodurch die Erstellung wiederverwendbarer Komponenten beliebiger Komplexität möglich wird.


Punkt drei – „for-Schleifen“. In Nui gibt es ein integriertes <for> -Tag, mit dem Sie Iterationen verwenden können, um dieselben (oder mehrere) Komponenten mehrmals darzustellen. Dies ist praktisch, wenn ein Datensatz vorhanden ist, aus dem Sie eine Liste/Zeile/Spalte mit Widgets erstellen müssen.


Viertens - bedingtes Rendering. Auf Layoutebene wird das Tag <show> implementiert (es gab die Idee, es <if> zu nennen), das es Ihnen ermöglicht, verschachtelte Komponenten zu zeichnen oder sie unter verschiedenen Bedingungen überhaupt nicht in den Baum einzubetten.


Punkt fünf – Aktionen. Einige Komponenten, mit denen der Benutzer interagieren kann, können Ereignisse senden . Die Sie vollständig nach Belieben steuern können. Nehmen wir an, <inkWell onPressed="something"> – mit einer solchen Deklaration wird dieses Widget anklickbar und Ihre Anwendung, oder besser gesagt, ein EventHandler , kann dieses Ereignis verarbeiten und etwas tun. Die Idee ist, dass alles, was mit der Logik zu tun hat, direkt in der Anwendung implementiert werden sollte, aber Sie können alles implementieren. Erstellen Sie einige generische Handler, die Aktionsgruppen verarbeiten können, wie „Zum Bildschirm gehen“ / „Methode aufrufen“ / „Analyseereignis senden“. Es gibt Pläne, auch dynamischen Code zu implementieren, aber hier gibt es Nuancen. Für Dart gibt es Möglichkeiten, beliebigen Code auszuführen, aber dies wirkt sich auf die Leistung aus, und außerdem beträgt die Interoperabilität dieses Codes mit dem Anwendungscode kaum 100 %. Das heißt, wenn Sie Logik in diesem dynamischen Code erstellen, werden Sie ständig auf einige Einschränkungen stoßen. Daher muss dieser Mechanismus sehr sorgfältig ausgearbeitet werden, um wirklich anwendbar und nützlich zu sein.


Der sechste Punkt ist die lokale Aktualisierung der Benutzeroberfläche. Dies ist dank des Tags <dataBuilder> möglich. Dieses Tag (im Grunde Bloc) kann sich ein bestimmtes Feld „anschauen“ und bei einer Änderung dessen Unterbaum neu zeichnen.


Daten

Anfangs bin ich dem Weg zweier Datenspeicher gefolgt – dem oben erwähnten „übergeordneten Kontext“. Und „Daten“ – Daten, die direkt in der Benutzeroberfläche mithilfe des Tags <data> definiert werden können. Ehrlich gesagt kann ich mich jetzt nicht mehr an die Argumentation erinnern, warum es notwendig war, zwei Möglichkeiten zum Speichern und Übertragen von Daten an die Benutzeroberfläche zu implementieren, aber ich kann mich für eine solche Entscheidung nicht wirklich harsch kritisieren.


Sie funktionieren wie folgt: Der „übergeordnete Kontext“ ist ein Objekt vom Typ Map<String, dynamic> , das direkt an die Widgets NuiListWidget / NuiStackWidget übergeben wird. Der Zugriff auf diese Daten ist über das Präfix page möglich:

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

Sie können auf alles verweisen, in beliebiger Tiefe, einschließlich Arrays - {{ page.some.array.0.users.35.age }} . Wenn es keinen solchen Schlüssel/Wert gibt, erhalten Sie null . Listen können mit <for> durchlaufen werden.


Die zweite Möglichkeit - "Daten" ist ein globaler Datenspeicher. In der Praxis ist dies ein bestimmter Bloc , der sich höher im Baum befindet als NuiListWidget / NuiStackWidget . Gleichzeitig hindert nichts daran, ihre Verwendung in einem lokalen Stil zu organisieren und Ihre eigene Instanz von DataStorage über DataStorageProvider zu übergeben.


Gleichzeitig ist die erste Methode nicht reaktiv - das heißt, wenn sich die Daten auf page ändern, wird sich keine Benutzeroberfläche aktualisieren. Denn dies sind tatsächlich nur die Argumente Ihres StatelessWidget . Wenn die Datenquelle für page beispielsweise Ihr eigener Bloc ist, der Nui...Widget einen Satz von Werten zuweist, wird es wie bei einem regulären StatelessWidget vollständig mit neuen Daten neu gezeichnet.


Die zweite Möglichkeit, mit Daten zu arbeiten, ist reaktiv. Wenn Sie die Daten in DataStorage ändern, indem Sie die API dieser Klasse verwenden - die Methode updateValue , wird die emit Methode der Klasse Bloc aufgerufen, und wenn in Ihrer Benutzeroberfläche aktive Listener dieser Daten vorhanden sind - <dataBuilder> -Tags -, wird deren Inhalt entsprechend geändert, aber der Rest der Benutzeroberfläche wird nicht berührt.


Somit erhalten wir zwei potenzielle Datenquellen - eine sehr einfache page und reaktive data . Abgesehen von der Logik der Datenaktualisierung in diesen Quellen und der Reaktion der Benutzeroberfläche auf diese Aktualisierungen gibt es keinen Unterschied zwischen ihnen.

Dokumentation

Ich habe absichtlich nicht alle Nuancen und Aspekte der Arbeit beschrieben, da es sich sonst um eine Kopie der bereits vorhandenen Dokumentation handeln würde. Wenn Sie also Interesse haben, es auszuprobieren oder einfach mehr zu erfahren, sind Sie hier genau richtig. Wenn irgendwelche Aspekte der Arbeit nicht klar sind oder die Dokumentation etwas nicht abdeckt, würde ich mich über Ihre Nachricht mit der Angabe des Problems freuen:


Ich werde kurz einige der Funktionen auflisten, die in diesem Artikel nicht behandelt werden, Ihnen aber zur Verfügung stehen:

  • Erstellen Sie Ihre eigenen Tags/Komponenten mit der Möglichkeit, für sie genau dieselbe interaktive Dokumentation zu erstellen wie für ihre Argumente und Eigenschaften mit Live-Vorschau. So wird beispielsweise die Komponente zum Rendern von SVG-Bildern implementiert. Es hat keinen Sinn, sie in den Kern der Engine zu schieben, da nicht jeder sie benötigt, aber als Erweiterung, die durch Übergeben nur einer Variablen verwendet werden kann, ist es einfach und unkompliziert. Direkt –ein Beispiel für die Implementierung .


  • Eine riesige integrierte Bibliothek mit Symbolen, die durch Hinzufügen eigener Symbole erweitert werden kann (hier war ich inkonsistent und habe „hineingeschubst“, die Logik bestand darin, so viele Symbole wie möglich sofort zur Verfügung zu stellen, und es war nicht erforderlich, die Anwendung zu aktualisieren, um neue Symbole zu verwenden). Standardmäßig sind verfügbar: fluentui_system_icons , material_design_icons_flutter und remixicon . Sie können alle verfügbaren Symbole mit Nanc , Page Interface / Screen -> Icons anzeigen.

  • Benutzerdefinierte Schriftarten, einschließlich standardmäßiger Google Fonts


  • Konvertieren von XML in JSON/protobuf und Verwenden als „Quellen“ für die Benutzeroberfläche


All dies und vieles mehr kann in der Dokumentation nachgelesen werden.


Was kommt als nächstes?

Die Hauptsache ist, die Möglichkeit auszuarbeiten, Code dynamisch mit Logik auszuführen. Dies ist eine sehr coole Funktion, mit der Sie die Fähigkeiten von Nui erheblich erweitern können. Außerdem können (und sollten) Sie die verbleibenden selten verwendeten, aber manchmal sehr wichtigen Widgets aus der Standard-Flutter-Bibliothek hinzufügen. XSD zu beherrschen, sodass die automatische Vervollständigung für alle Tags in der IDE angezeigt wird (es gibt eine Idee, dieses Schema direkt aus der Tag-Dokumentation zu generieren, dann ist es einfach, es für benutzerdefinierte Widgets zu erstellen, und es ist immer auf dem neuesten Stand, und es gibt auch eine Idee, eine generierte DSL in Dart zu erstellen, die dann in XML / Json / Protobuf konvertiert werden kann). Nun, und zusätzliche Leistungsoptimierung – es ist im Moment nicht schlecht, sehr nicht schlecht, aber es kann noch besser sein, sogar näher an nativem Flutter.


Das ist alles, was ich habe. Im nächsten Artikel werde ich ausführlich über die Leistung von Nui berichten, wie ich Testfälle erstellt habe, wie viele Dutzend Rakes ich dabei durchlaufen habe und welche Zahlen in welchen Szenarien erzielt werden können.


Wenn Sie Interesse daran haben, Nui auszuprobieren oder es besser kennenzulernen, wenden Sie sich bitte an den Dokumentationsschalter . Und wenn es nicht schwierig ist, dann setzen Sie bitte einen Stern auf GitHub und ein „Gefällt mir“ auf pub.dev. Für Sie ist es nicht schwierig, aber für mich, einen einsamen Ruderer auf diesem riesigen Boot, ist es unglaublich nützlich.