Dies ist der zweite Teil meiner Serie über Flatterarchitektur:
Streams sind der Hauptbaustein von RxVMS . Ihr Verständnis ist für die Arbeit mit dieser Bibliothek unbedingt erforderlich. Wir werden uns in diesem Beitrag ausführlicher mit ihnen befassen .
Es stellte sich heraus, dass die Aufnahme von Rx in diesen Beitrag zu lang werden würde, also habe ich es in zwei Teile geteilt.
Lass es fließen
Ich habe viele Kommentare gelesen, denen zufolge Streams und insbesondere Rx zu kompliziert sind, um sie zu verstehen und daher zu verwenden.
Ich möchte, dass Sie wissen, dass ich mich nicht als Rx-Guru betrachte. Es ist nicht einfach, all seine Kräfte zu nutzen, und ich gebe zu, dass ich weiter lerne. Aber lassen Sie mich von Anfang an einen Fehler beheben: Sie müssen kein Rx-Assistent sein, um eine Menge Vorteile aus der Verwendung von Threads und dieser Technologie zu ziehen . Ich werde alle Anstrengungen unternehmen, um Ihnen die Abläufe auf die am besten zugängliche Weise zu erklären.
Was sind Threads?
Die beste Analogie zu Gewinden ist meiner Meinung nach das Förderband. Sie können etwas an einem Ende anbringen und dieses „Etwas“ wird automatisch auf das andere übertragen. Im Gegensatz zur physischen Pipeline manipulieren Threads Datenobjekte und übertragen sie automatisch von Anfang an - aber wo? Wie in der realen Pipeline werden die Daten am anderen Ende einfach "fallen" und verschwinden, wenn sie nicht erfasst werden (dies gilt natürlich nicht ganz für Dart-Streams, aber es ist am besten, Streams so zu behandeln, als ob dies der Fall wäre). .

Um Datenverlust zu vermeiden, können Sie einen "Trap" für die Stream-Ausgabe festlegen. Auf diese Weise können Sie Daten erfassen und die erforderlichen Manipulationen damit durchführen, wenn Datenobjekte das Ende des Streams erreichen.

Denken Sie daran:
- Wenn die Falle nicht gesetzt ist, verschwinden die Daten einfach für immer und es gibt keine Möglichkeit, sie erneut abzurufen (wieder nicht genau mit Dart Streams, aber Sie tun besser so, als ob es so wäre).
- Nachdem Sie Daten an den Stream gesendet haben, müssen Sie das Programm nicht anhalten und warten, bis es das Ende erreicht hat. Dies alles geschieht im Hintergrund.
- Der Trap kann jederzeit Daten empfangen, dies ist nicht sofort nach dem Senden erforderlich (aber keine Sorge, die Streams sind tatsächlich sehr schnell). Stellen Sie sich vor, Sie wissen nicht, wie schnell oder wie lange sich das Förderband bewegt. Dies bedeutet, dass das Platzieren von etwas im Stream völlig unabhängig von der Reaktion auf das Element am anderen Ende ist. Ihre Falle wird funktionieren und den Gegenstand fangen, wenn er dort ankommt. (Einige von Ihnen stellen möglicherweise bereits fest, dass dies gut zu der reaktiven Art und Weise passt, wie Flutter seine Widgets aktualisiert.)
- Sie können eine Falle stellen, lange bevor die Arbeit beginnt und das erste Element angezeigt wird
- Der Ablauf erfolgt nach dem FIFO-Prinzip. Die Daten kommen immer in der Reihenfolge, in der sie im Stream abgelegt werden.
Was ist Rx?
Rx, kurz für Reactive Extensions, sind Steroidströme. Dies ist ein Konzept, das Streams sehr ähnlich ist und vom Microsoft-Team für das .NET-Framework erfunden wurde. Da .Net bereits den Stream-Typ hatte, der für Datei-E / A verwendet wird, haben sie die Rx-Streams Observables aufgerufen und viele Funktionen erstellt, um die durch sie fließenden Daten zu bearbeiten. Dart hat Streams in seine Sprachspezifikation integriert, die bereits den größten Teil dieser Funktionalität bieten, jedoch nicht alle. Aus diesem Grund wurde das RxDart-Paket entwickelt. Es basiert auf Dart Streams, erweitert jedoch deren Funktionalität. Ich werde Rx und RxDart im nächsten Teil dieser Serie behandeln.
Einige Begriffe
Dart Streams und Rx verwenden eine Terminologie, die beängstigend aussehen kann. Hier ist die Übersetzung. Zuerst kommt der Begriff Dart, dann Rx.
- Stream / Observable . Dies ist die zuvor beschriebene "Pipeline". Stream kann in Observable konvertiert werden. Wo immer Stream erwartet wird, können Sie Observable zuweisen. Seien Sie also nicht verwirrt, wenn ich diese Begriffe während des Erklärungsprozesses mische
- listen / subscribe - Setzt die Listener- Falle
- StreamController / Betreff . Die "linke" Seite des Förderbandes, auf der Sie die Daten in den Stream einfügen. Sie unterscheiden sich geringfügig in ihren Eigenschaften und Merkmalen, dienen jedoch demselben Zweck.
- Senden eines Artikels / von Daten . Der Moment, in dem Daten am Ausgang der "Pipeline" erscheinen
Stream-Erstellung
Wenn Sie das Thema weiter studieren möchten, klonen Sie dieses Projekt bitte mit Beispielen. Ich werde das Dart / Flutter-Testsystem verwenden.
Um einen Stream zu erstellen, erstellen Sie einen StreamController
var controller = new StreamController<String>(); controller.add("Item1");
Der beim Erstellen des StreamController übergebene Vorlagentyp (in diesem Fall String) bestimmt den Objekttyp, den wir an den Stream senden können. Es kann jeder Typ sein! Wenn Sie möchten, können Sie einen StreamController<List<MyObject>>()
erstellen, und der Stream überträgt das gesamte Blatt anstelle eines einzelnen Objekts.
Trap-Einstellung
Wenn Sie den angegebenen Test ausgeführt haben, konnten Sie nichts sehen, da am Ausgang des Streams nichts unsere Zeile erfasst hat. Stellen Sie nun die Falle:
var controller = new StreamController<String>(); controller.stream.listen((item) => print(item));
Jetzt wird der Trap mit der Methode .listen()
. Die Aufnahme sieht aus wie controller.stream.listen
, aber wenn Sie sie rückwärts scrollen, wie eine Art Album aus den 60ern, wird die wahre Bedeutung des Geschriebenen angezeigt: "Hören Sie sich den Stream dieses Controllers an"
Sie müssen eine bestimmte Funktion an die Methode .listen()
, um die eingehenden Daten irgendwie zu manipulieren. Die Funktion muss einen Parameter des beim Erstellen des StreamControllers angegebenen Typs akzeptieren, in diesem Fall String.
Wenn Sie den obigen Code ausführen, werden Sie sehen
Item1 Item2 Item3
Meiner Meinung nach besteht das größte Problem für Stream-Neulinge darin, dass Sie die Reaktion für das emittierte Element bestimmen können, lange bevor das erste Element in den Stream aufgenommen wird, wodurch ein Aufruf dieser Reaktion ausgelöst wird.
Hör zu
Der obige Code hat den kleinen, aber wichtigen Teil übersehen. listen()
gibt ein StreamSubscription
- ein Stream-Abonnementobjekt. Ein Aufruf seiner .cancel()
-Methode beendet das Abonnement, setzt Ressourcen frei und verhindert, dass Ihre Abhörfunktion aufgerufen wird, nachdem sie nicht mehr benötigt wird.
var controller = new StreamController<String>(); StreamSubscription subscription = controller.stream.listen((item) => print(item));
Hörer Details
Die Funktion für listen()
kann entweder ein Lambda oder eine einfache Funktion sein.
void myPrint(String message) { print(message); } StreamSubscription subscription = controller.stream.listen((item) => print(item));
Wichtiger Hinweis: Die meisten Dart-Streams erlauben nur ein einmaliges Abonnement, dh sie können nach Abschluss des Abonnements nicht erneut abonniert werden. Dies löst eine Ausnahme aus. Dies ist ihr Unterschied zu anderen Rx-Implementierungen.
Die vollständige Signatur von listen()
sieht folgendermaßen aus:
StreamSubscription<T> listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError});
Dies bedeutet, dass Sie mehr als nur einen Handler für die gesendeten Daten übergeben können. Sie können auch einen Handler für Fehler und einen anderen zum Schließen des Streams vom Controller ( onDone
) haben. Ausnahmen, die aus dem Stream onError()
werden onError()
wenn Sie es bereitstellen. Andernfalls werden sie einfach verschluckt und Sie werden nie erfahren, dass ein onError()
.
Beispiel für Flattergewinde
Um das Verständnis der folgenden Kapitel zu erleichtern, habe ich einen separaten Repository-Zweig erstellt.
Bitte klone sie
Als erstes Beispiel habe ich die bekannte Zähleranwendung, die Sie beim Erstellen eines neuen Flutter-Projekts erhalten, genommen und ein wenig neu organisiert. Ich habe eine Modellklasse hinzugefügt, um den Status der Anwendung zu speichern. Dies ist im Grunde ein Zählerwert:
class Model { int _counter = 0; StreamController _streamController = new StreamController<int>(); Stream<int> get counterUpdates => _streamController.stream; void incrementCounter() { _counter++; _streamController.add(_counter); } }
Hier sehen Sie eine sehr typische Vorlage: Anstatt den gesamten StreamController zu veröffentlichen, veröffentlichen wir einfach seine Stream-Eigenschaft.
Um das Modell für die Benutzeroberfläche verfügbar zu machen, habe ich es zu einem statischen Feld im App-Objekt gemacht, da ich weder InheritedWidget noch ServiceLocator eingeben wollte. Für ein einfaches Beispiel wird dies damit durchkommen, aber ich würde es in dieser Anwendung nicht tun!
Zu main.dart
:
class _MyHomePageState extends State<MyHomePage> { int _counter = 0; StreamSubscription streamSubscription; @override void initState() { streamSubscription = MyApp.model.counterUpdates.listen((newVal) => setState(() { _counter = newVal; })); super.initState(); }
initState()
guter Ort, um den Listener einzustellen, und als gute Bürger von Darts veröffentlichen wir immer ein Abonnement, dispose()
zu dispose()
, oder?
Im Widget-Baum müssen wir nur den onPressed-Handler der FAB-Schaltfläche anpassen (Schaltfläche mit schwebender Aktion).
floatingActionButton: new FloatingActionButton( onPressed: MyApp.model.incrementCounter, tooltip: 'Increment', child: new Icon(Icons.add), ),
Auf diese Weise haben wir mithilfe von Stream eine saubere Trennung zwischen Ansicht und Modell erstellt.
Wenden Sie StreamBuilder an
Quelle
Anstatt initState()
und setState()
für unsere Anforderungen zu verwenden, wird Flutter mit einem praktischen StreamBuilder
Widget StreamBuilder
. Wie Sie vielleicht erraten haben, sind eine Stream-Funktion und eine Konstruktormethode erforderlich, die aufgerufen werden, wenn Stream einen neuen Wert zurückgibt. Und jetzt brauchen wir keine explizite Initialisierung und Freigabe:
body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Text( 'You have pushed the button this many times:', ), StreamBuilder<int>( initialData: 0, stream: MyApp.model.counterUpdates, builder: (context, snappShot) { String valueAsString = 'NoData'; if (snappShot != null && snappShot.hasData) { valueAsString = snappShot.data.toString(); } return Text( valueAsString, style: Theme.of(context).textTheme.display1, ); }), ], ), ),
Wir sind fast fertig, das verspreche ich. Hier sind drei Dinge, die Sie wissen sollten:
- Der große Vorteil der Verwendung von StreamBuilder gegenüber der ersten Lösung besteht darin, dass durch das Aufrufen von
setState()
in listen()
immer die gesamte Seite neu angeordnet wird, während StreamBuilder nur den builder
aufruft - Die Variable
snapShot
enthält die neuesten von Stream empfangenen Daten. Stellen Sie immer sicher, dass es gültige Daten enthält, bevor Sie es verwenden. Basierend auf den Prinzipien der Initialisierung während kann StreamBuilder im ersten Frame keinen Wert erhalten. Um dies zu initialData
, übergeben wir den Wert für initialData
, der für die erste Assembly verwendet wird, initialData
für den ersten Frame des Bildschirms. Wenn wir initialData
nicht übergeben, wird unser Builder zum ersten Mal mit ungültigen Daten aufgerufen. Eine Alternative zur Verwendung von initialData
besteht darin, ein Platzhalter-Widget zurückzugeben, wenn snapShot
ungültig ist. snapShot
wird angezeigt, bis wir gültige Daten erhalten, zum Beispiel:
Im nächsten Beitrag werden wir uns ansehen, wie Daten in unseren Streams konvertiert und im laufenden Betrieb ausgeführt werden. Vielen Dank an Scott Stoll für das Lesen der Beweise und das wichtige Feedback.