Vor kurzem musste ich Onboarding-Walkthroughs für ClickUp-Neulinge implementieren! Das war eine wirklich wichtige Aufgabe, da viele neue Benutzer die Plattform dank der unglaublich lustigen Werbung, die wir beim Super Bowl vorgestellt haben, entdecken würden! ✨
via ClickUp
Die Anleitung ermöglicht es unseren zahlreichen neuen Benutzern, die ClickUp vielleicht noch nicht kennen, schnell zu verstehen, wie sie verschiedene Funktionen der Anwendung nutzen können. Es handelt sich um einen fortlaufenden Aufwand, genau wie die neue ClickUp University-Ressource, die wir derzeit entwickeln! 🚀
Glücklicherweise konnte ich dank der Softwarearchitektur hinter der ClickUp Flutter-Mobil-App diese Funktion recht schnell implementieren, sogar unter Wiederverwendung der echten Widgets aus der App! Das bedeutet, dass die Anleitung dynamisch und reaktionsschnell ist und genau den echten Anwendungsbildschirmen der App entspricht – und dies auch weiterhin tun wird, selbst wenn sich die Widgets weiterentwickeln.
Dank der richtigen Aufgabentrennung konnte ich die Funktionen auch implementieren.
Schauen wir uns einmal an, was ich damit meine. 🤔
Trennung von Belangen
Das Entwerfen einer Softwarearchitektur ist eines der komplexesten Themen für Entwicklerteams. Unter all den Aufgaben ist es immer schwierig, zukünftige Softwareentwicklungen vorherzusehen. Deshalb kann die Erstellung einer gut geschichteten und entkoppelten Architektur Ihnen und Ihren Teamkollegen in vielerlei Hinsicht helfen!
Der Hauptvorteil der Erstellung kleiner, entkoppelter Systeme ist zweifellos die Testbarkeit! Und genau das hat mir geholfen, eine Demo-Alternative der bestehenden Bildschirme aus der App zu erstellen!
Eine Schritt-für-Schritt-Anleitung
Wie können wir diese Prinzipien nun auf eine Flutter-Anwendung anwenden?
Wir geben einige Techniken frei, die wir zum Erstellen von ClickUp verwenden, anhand eines einfachen Beispiels.
Das Beispiel ist so einfach, dass es vielleicht nicht alle Vorteile verdeutlicht, aber glauben Sie mir, es wird Ihnen helfen, viel besser wartbare Flutter-Anwendungen mit komplexen Codebasen zu erstellen. 💡
Die Anwendung
Als Beispiel erstellen wir eine Anwendung, die die Population der USA für jedes Jahr anzeigt.
via ClickUp
Wir haben hier zwei Bildschirme:
- HomeScreen: Die Liste enthält einfach alle Jahre von 2000 bis heute. Wenn der Benutzer auf eine Jahreskachel tippt, gelangt er zum DetailScreen mit einem Navigationsargument, das auf das ausgewählte Jahr gesetzt ist.
- DetailScreen: Ruft das Jahr aus dem Navigationsargument ab, ruft die datausa.io-API für dieses Jahr auf und analysiert die JSON-Daten, um den zugehörigen Wert der Population zu extrahieren. Wenn Daten verfügbar sind, wird eine Beschreibung mit dem Wert der Population angezeigt.
Wir konzentrieren uns auf die Implementierung von DetailScreen, da diese mit ihrem asynchronen Aufruf am interessantesten ist.
Schritt 1. Naiver Ansatz

Die naheliegendste Implementierung für unsere App ist die Verwendung eines einzigen StatefulWidget für die gesamte Logik.
Zugriff auf das Argument für die Jahresnavigation
Um auf das angeforderte Jahr zuzugreifen, lesen wir die RouteSettings aus dem geerbten Widget ModalRoute.
HTTP-Aufruf
In diesem Beispiel wird die get-Funktion aus dem http-Paket aufgerufen, um die Daten aus der datausa.io-API abzurufen, das Ergebnis der JSON-Datei mit der jsonDecode-Methode aus der dart:convert-Bibliothek zu analysieren und Future als Teil des Status mit einer Eigenschaft namens _future zu behalten.
Rendering
Um den Widget-Baum zu erstellen, verwenden wir einen FutureBuilder, der sich entsprechend dem aktuellen Status unseres asynchronen _future-Aufrufs neu aufbaut.
Überprüfen
Okay, die Implementierung ist kurz und verwendet nur integrierte Widgets, aber denken Sie nun an unsere ursprüngliche Absicht: die Erstellung von Demo-Alternativen (oder Tests) für diesen Bildschirm. Es ist sehr schwierig, das Ergebnis des HTTP-Aufrufs zu steuern, um die Anwendung zu zwingen, in einem bestimmten Zustand zu rendern.
Hier kommt uns das Konzept der Inversion of Control zu Hilfe. 🧐
Schritt 2. Umkehrung der Kontrolle

Dieses Prinzip kann für neue Entwickler schwer zu verstehen sein (und auch schwer zu erklären), aber die Grundidee besteht darin, die Probleme außerhalb unserer Komponenten zu extrahieren, damit diese nicht für die Auswahl des Verhaltens verantwortlich sind, und sie stattdessen zu delegieren.
In einer häufigeren Situation besteht dies einfach darin, Abstraktionen zu erstellen und Implementierungen in unsere Komponenten einzufügen, damit deren Implementierung später bei Bedarf geändert werden kann.
Aber keine Sorge, nach unserem nächsten Beispiel wird alles klarer! 👀
Erstellen eines API-Client-Objekts
Um den HTTP-Aufruf an unsere API zu steuern, haben wir unsere Implementierung in einer speziellen DataUsaApiClient-Klasse isoliert. Außerdem haben wir eine Measure-Klasse erstellt, um die Bearbeitung und Pflege von Daten zu vereinfachen.
Bereitstellung eines API-Clients
In unserem Beispiel verwenden wir das bekannte Anbieter -Paket, um eine DataUsaApiClient-Instanz in den Stamm unseres Baums einzufügen.
Verwendung des API-Clients
Der Anbieter ermöglicht es jedem untergeordneten Widget (wie unserem DetailScreen) das nächstgelegene DataUsaApiClient-Oberteil im Baum zu lesen. Wir können dann seine getMeasure-Methode verwenden, um unser Future zu starten, anstelle der eigentlichen HTTP-Implementierung.
Demo-API-Client
Jetzt können wir davon profitieren!
Falls Sie es noch nicht wussten: Alle Klassen in Dart definieren implizit auch eine zugehörige Schnittstelle. Dadurch können wir eine alternative Implementierung von DataUsaApiClient bereitstellen, die bei Aufrufen ihrer getMeasure-Methode immer dieselbe Instanz zurückgibt.
Diese Methode
Diese Methode
Anzeigen einer Demoseite
Wir haben nun alle Schlüssel, um eine Demo-Instanz der DetailPage anzuzeigen!
Wir überschreiben einfach die derzeit bereitgestellte DataUsaApiClient-Instanz, indem wir unseren DetailScreen in einen Anbieter einbinden, der stattdessen eine DemoDataUsaApiClient-Instanz erstellt!
Und das war's auch schon – unser DetailScreen liest stattdessen diese Demo-Instanz und verwendet unsere DemoMeasure-Daten anstelle eines HTTP-Aufrufs.
Überprüfen
Dies ist ein großartiges Beispiel für Inversion of Control. Unser DetailScreen-Widget ist nicht mehr für die Logik des Abrufs der Daten verantwortlich, sondern delegiert diese Aufgabe an ein spezielles Client-Objekt. Und wir sind nun in der Lage, Demo-Instanzen des Bildschirms zu erstellen oder Widget-Tests für unseren Bildschirm zu implementieren! Fantastisch! 👏
Aber wir können noch mehr erledigen!
Da wir als Beispiel keinen Ladezustand simulieren können, haben wir keine vollständige Kontrolle über Zustandsänderungen auf Widget-Ebene.
Schritt 3. Zustandsverwaltung

Dies ist ein heißes Thema in Flutter!
Sicherlich haben Sie schon lange Threads von Leuten gelesen, die versuchen, die beste Lösung für das State-Management in Flutter zu finden. Um es klar zu sagen: Das ist nicht das Thema dieses Artikels, das zu erledigen ist. Unserer Meinung nach ist alles in Ordnung, solange Sie Ihre Geschäftslogik von Ihrer visuellen Logik trennen! Die Erstellung dieser Ebenen ist für die Wartbarkeit wirklich wichtig. Unser Beispiel ist einfach, aber in realen Anwendungen kann die Logik schnell komplex werden, und eine solche Trennung macht es viel einfacher, Ihre reinen Logikalgorithmen zu finden. Dieses Thema wird oft als Zustandsverwaltung zusammengefasst.
In diesem Beispiel verwenden wir einen einfachen ValueNotifier zusammen mit einem Anbieter. Wir hätten aber auch flutter_bloc oder riverpod (oder eine andere Lösung) verwenden können, und das hätte ebenfalls hervorragend funktioniert. Die Prinzipien bleiben dieselben, und solange Sie Ihre Zustände und Ihre Logik getrennt haben, ist es sogar möglich, Ihre Codebasis aus einer der anderen Lösungen zu portieren.
Diese Trennung hilft uns auch dabei, jeden Status unserer Widgets zu kontrollieren, sodass wir ihn auf jede erdenkliche Weise simulieren können!
Erstellen eines dedizierten Status
Anstatt uns auf den AsyncSnapshot aus dem Framework zu verlassen, stellen wir unseren Bildschirmstatus nun als DetailState-Objekt dar.
Es ist auch wichtig, die Methoden hashCode und operator == zu implementieren, um unser Objekt nach seinem Wert vergleichbar zu machen. So können wir erkennen, ob zwei Instanzen als unterschiedlich betrachtet werden sollten.
💡 Die equatable- oder freezed- Pakete sind großartige Optionen, die Ihnen bei der Implementierung der Methoden hashCode und operator == helfen!
💡 Die equatable- oder freezed- Pakete sind großartige Optionen, die Ihnen bei der Implementierung der Methoden hashCode und operator == helfen!
Unser Status ist immer mit einem Jahr verbunden, aber wir haben auch vier verschiedene mögliche Status in Bezug auf das, was wir dem Benutzer anzeigen möchten:
- NotLoadedDetailState: Die Datenaktualisierung hat noch nicht begonnen.
- LoadingDetailState: Die Daten werden derzeit geladen.
- LoadedDetailState: Die Daten wurden mit einer zugehörigen Messgröße erfolgreich geladen.
- NoDataDetailState: Die Daten wurden geladen, aber es sind keine Daten verfügbar.
- UnknownErrorDetailState: Der Vorgang ist aufgrund eines unbekannten Fehlers fehlgeschlagen.
Diese Zustände sind klarer als ein AsyncSnapshot, da sie unsere Anwendungsfälle wirklich repräsentieren. Und auch dies macht unseren Code wartungsfreundlicher!
💡 Wir empfehlen Ihnen dringend, die Union-Typen aus dem Freezed-Paket zu verwenden, um Ihre Logikzustände darzustellen! Es fügt viele nützliche Funktionen wie die Methoden „copyWith” oder „map” hinzu.
💡 Wir empfehlen Ihnen dringend, die Union-Typen aus dem Freezed-Paket zu verwenden, um Ihre Logikzustände darzustellen! Es fügt viele nützliche Funktionen wie die Methoden „copyWith” oder „map” hinzu.
Die Logik in einen Notifier einfügen
Nachdem wir nun eine Darstellung unseres Status haben, müssen wir eine Instanz davon irgendwo speichern – das ist der Zweck des DetailNotifier. Er speichert die aktuelle DetailState-Instanz in seiner Wert-Eigenschaft und stellt Methoden zur Aktualisierung des Status bereit.
Wir bieten einen NotLoadedDetailState-Ausgangszustand und eine Aktualisierungsmethode, um Daten aus der API zu laden und den aktuellen Wert zu aktualisieren.
Stellen Sie einen Status für die Ansicht bereit
Um den Status unseres Bildschirms zu instanziieren und zu beobachten, verlassen wir uns auch auf den Anbieter und seinen ChangeNotifierProvider. Diese Art von Anbieter sucht automatisch nach allen erstellten ChangeListenern und ist als Auslöser für jeden Aufruf des Consumers zuständig, bei dem er über eine Änderung benachrichtigt wird (wenn sich der Wert unseres Notifiers vom vorherigen unterscheidet).
Überprüfen
Großartig! Unsere Anwendungsarchitektur sieht langsam ziemlich gut aus. Alles ist in klar definierte Schichten mit spezifischen Aufgabenbereichen unterteilt! 🤗
Für die Testbarkeit fehlt jedoch noch eine Sache: Wir möchten den aktuellen DetailState definieren, um den Status unseres zugehörigen DetailScreen zu steuern.
Schritt 4. Visuelles Widget für dedizierte Layouts

Im letzten Schritt haben wir unserem DetailScreen-Widget etwas zu viel Verantwortung übertragen: Es war für die Instanziierung des DetailNotifier verantwortlich. Und wie wir bereits gesehen haben, versuchen wir, jegliche logische Verantwortung auf der Ansichtsebene zu vermeiden!
Wir können dies ganz einfach lösen, indem wir eine weitere Ebene für unser Bildschirm-Widget erstellen: Wir teilen unser DetailScreen-Widget in zwei Teile auf:
- DetailScreen ist für die Einrichtung der verschiedenen Abhängigkeiten unseres Bildschirms aus dem aktuellen Anwendungsstatus (Navigation, Benachrichtigungen, Status, Dienste usw.) verantwortlich.
- DetailLayout wandelt einen DetailState einfach in einen dedizierten Baum von Widgets um.
Durch die Kombination beider Elemente können wir auf einfache Weise DetailLayout-Demo-/Test-Instanzen erstellen, verfügen jedoch über DetailScreen für den tatsächlichen Anwendungsfall in unserer Anwendung.
Spezielle Layouts
Um eine bessere Trennung der Aufgabenbereiche zu erreichen, haben wir alles unter dem Consumer-Widget in ein spezielles DetailLayout-Widget verschoben. Dieses neue Widget verbraucht nur Daten und ist nicht für die Instanziierung verantwortlich. Es konvertiert lediglich den Lesestatus in einen bestimmten Widget-Baum.
Die ModalRoute. von call und ChangeNotifierProvider-Instanz verbleibt im DetailScreen, und dieses Widget gibt einfach das DetailLayout mit einem vorkonfigurierten Abhängigkeitsbaum zurück!
Diese geringfügige Verbesserung ist spezifisch für die Verwendung durch Anbieter, aber Sie werden feststellen, dass wir auch einen ProxyProvider hinzugefügt haben, sodass jedes untergeordnete Widget direkt einen DetailState verwenden kann. Dies erleichtert das Mocken von Daten.
Extrahieren von Widgets als dedizierte Klassen
Zögern Sie nicht, einen Widget-Baum in eine eigene Klasse zu extrahieren! Dies verbessert die Leistung und macht den Code wartungsfreundlicher.
In unserem Beispiel haben wir für jeden der zugehörigen Status-Typen ein visuelles Layout-Widget erstellt:
Demo-Instanzen
Jetzt haben wir die volle Kontrolle darüber, was wir simulieren und auf unserem Bildschirm anzeigen können!
Wir müssen lediglich ein DetailLayout mit einem Anbieter-
Fazit
Die Erstellung einer wartbaren Softwarearchitektur ist definitiv nicht einfach! Die Vorhersage zukünftiger Szenarien kann viel Aufwand erfordern, aber ich hoffe, dass Ihnen die wenigen Tipps, die ich Ihnen freigegeben habe, in Zukunft helfen werden!
Die Beispiele mögen einfach aussehen – vielleicht sogar so, als würden wir zu viel Engineering betreiben –, aber wenn Ihre App komplexer wird, werden Ihnen diese Standards sehr helfen! 💪
Viel Spaß mit Flutter und folgen Sie dem Blog, um weitere technische Artikel wie diesen zu erhalten! Bleiben Sie dran!

