Asynchronous programming in Flutter

Moderne mobile Apps ohne asynchronen Code zu entwickeln, ist undenkbar. Die meisten Aufgaben, die eine Anwendung ausführen muss, erfordern in gewissem Maße ein langes Warten auf das Ergebnis der Operation: Netzwerkanfragen, Datenbankoperationen und das Einlesen von Benutzereingaben. Ein asynchroner Ansatz (Ausführung eines Prozesses ohne Blockierung) ermöglicht eine effizientere Nutzung der Ressourcen des Geräts. Angesichts der Einschränkungen mobiler Geräte ist asynchrone Entwicklung in Android und iOS unerlässlich.

In diesem Artikel betrachten wir die Implementierung von Asynchronität unter Verwendung der Funktionen der Dart-Sprache und des ’dart: async’ Pakets. Der Fokus liegt dabei hauptsächlich auf den Klassen Future und Stream.

Der Dart-Code-Ausführungsmechanismus

Dart, die Sprache zur Erstellung von Flutter-Anwendungen, ist eine Ein-Thread-Sprache. Dennoch stehen Werkzeuge wie Streams, Futures und async/await-Operatoren zur Verfügung. Diese sind den gleichen Elementen in anderen Programmiersprachen, insbesondere Java und Javascript, ähnlich, weisen jedoch einige Besonderheiten auf. Daher muss man zuerst verstehen, wie der Dart-Code-Ausführungsmechanismus funktioniert.

Dart-Code wird in separaten Speicherblöcken, sogenannten Isolates, ausgeführt. Theoretisch können alle Dart-Anwendungen in einem Isolat laufen. Falls jedoch mehrere Threads benötigt werden, kann die Dart-VM mehrere Isolate mit eigenem zugewiesenem Speicher erstellen. Im Gegensatz zu vielen Programmiersprachen wie Java oder C++ können in Dart verschiedene Threads nicht direkt auf denselben Speicherbereich zugreifen. Sie können Nachrichten austauschen, um die Daten des jeweils anderen zu verwenden, aber sie können nicht direkt mit dem Speicher arbeiten. Dies ist eine robustere Multithreading-Implementierung, die kein Locking erfordert und die Speicherverwaltung vereinfacht. Innerhalb jedes Isolates gibt es eine Ereignisschleife, die für die Handhabung von Ereignissen verantwortlich ist. Sie verarbeitet eingehende Ereignisse und kann die Ausführung von Ereignissen, die einen asynchronen Ansatz erfordern, aufschieben. Dies löst das Multithreading-Dilemma in einer nativ ein-Thread-Umgebung.

Wie sollte unsere Dart-Anwendung der Ereignisschleife mitteilen, dass dieser Codeabschnitt asynchron ausgeführt wird? Schauen wir uns zwei der häufigsten Ansätze für asynchrone Programmierung in Flutter an.

Futures

Die Arbeitsweise von Future ist der von Promise aus Javascript sehr ähnlich. Es hat zwei Zustände: unerledigt und abgeschlossen. Der abgeschlossene Future hat entweder einen Wert (bei Erfolg) oder einen Fehler (bei Misserfolg). Die Klasse hat mehrere Konstruktoren:

  1. Future.delayed (nimmt ein Duration-Objekt als Argument, das das Zeitintervall angibt, und eine Funktion, die nach der Verzögerung ausgeführt werden soll).
  2. Encodierter Speichercache (speichert komprimierte Bilder im Originalzustand im Speicher).
  3. Future.error (erstellt einen Future, der mit einem Fehler abgeschlossen wird).
  4. Future.microtask (gibt einen Future mit dem Ergebnis der durch scheduleMicrotask angegebenen Berechnung zurück).
  5. Future.sync (Future, das sofort endet).
  6. Future.value (Future, das den angegebenen Wert zurückgibt).

In der kommerziellen Entwicklung wird von den obigen Beispielen Future.delayed am häufigsten zur Implementierung aufgeschobener Aufgaben verwendet.

Der häufigere und einfachere Weg, einen Future zu erstellen, besteht darin, das async-Schlüsselwort in einer Funktion zu verwenden. Es gibt dann den Wert, der in einem Future eingeschlossen ist, zurück.

Ein Beispiel für die Verwendung von async

Es gibt mehrere Möglichkeiten, wie Sie das Ergebnis dieser Funktion in Flutter verwenden können.

1) Die einfachste Möglichkeit ist, das Ergebnis der Funktion mit dem await-Schlüsselwort zu erhalten. Ein wichtiger Punkt: await kann nur in Funktionen mit dem async-Schlüsselwort verwendet werden.

2) Verwendung von Callbacks .then und .catchError. Mit dieser Option wird die Arbeit mit Dart Future der Verwendung von Promise in Javascript so ähnlich wie möglich. Dieses Beispiel gibt den Benutzernamen bei Erfolg in der Konsole aus oder einen Fehler bei Misserfolg.

Dart Future verfügt auch über mehrere statische Methoden, die die Arbeit mit asynchronen Codes erleichtern: wait (nimmt eine Liste von Futures als Argument und gibt einen Future zurück, der auf ihre Fertigstellung wartet), any (nimmt eine Liste von Futures als Argument und gibt einen Future zurück, der auf das erste Element wartet), forEach (nimmt eine Liste von Futures als Argument und wendet die angegebene Funktion auf jedes Element an), doWhile (nimmt eine Funktion, die ein bool zurückgibt und führt sie aus, bis die Funktion false zurückgibt).

Es gibt auch 2 nicht-statische Funktionen – whenComplete (wird ausgeführt, wenn der Future abgeschlossen ist, unabhängig von Erfolg oder Misserfolg) und timeout (führt die Funktion nach einem Zeitintervall aus). Alle diese Funktionen können kombiniert werden, um komplexe Ketten zu erstellen.

3) Verwenden Sie das FutureBuilder Widget aus dem Flutter SDK. Sein Vorteil ist die Fähigkeit, den Zustand der Zukunft bei der Erstellung der Benutzeroberfläche zu berücksichtigen. Im folgenden Beispiel werden wir 3 UI-Optionen für jeden nameFuture-Status erstellen: Fortschrittsbalken zum Warten, Klartext bei Erfolg und Benachrichtigung bei Fehler.

Streams

Der zweite Teil der asynchronen Programmierung in Dart und Flutter sind Streams. Sie sind Sequenzen asynchroner Ereignisse. Sie sind in Namen und Inhalt identisch mit Streams aus anderen Programmiersprachen wie Java. In Dart werden Streams in zwei Typen unterteilt: mit einer einzigen Abonnement (ermöglicht nur einen Abonnenten und sendet Ereignisse nur, wenn einer vorhanden ist) und Broadcast (ermöglicht mehrere Abonnenten und sendet Ereignisse unabhängig von ihrer Anwesenheit). Wie Future enthält auch Stream potenzielle Werte. Diese Klasse enthält eine große Anzahl von Methoden zum Transformieren von Streams und zum Manipulieren ihrer Elemente. Sie können es, wie im Fall von Future, auf verschiedene Weisen erstellen:

1) Verwendung des Konstruktors: Standard Stream(), Stream.empty (erstellt einen leeren Stream), Stream.error (erstellt einen Stream, der einen Fehler auslöst), Stream.eventTransformed (nimmt einen Stream und modifiziert alle seine Elemente), Stream.fromFuture (wandelt einen einzelnen Future in einen Stream um), Stream.fromFutures (das Gleiche, akzeptiert jedoch eine Iterable von Futures), Stream.fromIterable (wandelt eine Liste von Objekten in einen Stream um), Stream.periodic (erstellt einen Stream, der regelmäßig Ereignisse sendet), Stream.value (erstellt einen Stream, der ein Element zurückgibt und beendet). In der kommerziellen Entwicklung ist diese Methode zur Erstellung von Streams nicht üblich.

2) Verwendung der Schlüsselwörter async* (ausgesprochen [əˈsɪŋk stɑː], gibt einen Stream zurück), yield (gibt ein Element eines Streams zurück) und yield* (ausgesprochen [jiːld stɑː], gibt einen Stream zurück).

Ein Beispiel für eine Autorisierung, die einen Stream von Zuständen zurückgibt

3) Verwendung von StreamController. Diese Klasse ist speziell für die Manipulation von Streams konzipiert und hat zwei Varianten: die Standardvariante (steuert Einzelabonnement-Streams) und Broadcast (steuert Broadcast-Streams).

In diesem Beispiel wird ein Stream mit einem Controller erstellt:

StreamController verfügt über eine Reihe nützlicher Funktionen und Attribute zur Manipulation von Streams. Dazu gehören die bereits erwähnten add() und close(), sowie addError (der Stream löst einen Fehler aus), addStream (fügt Ereignisse aus einem anderen Stream hinzu), stream (Verweis auf den erstellten Stream), sink (Verweis auf das Objekt, das für den Empfang neuer Daten verantwortlich ist) usw.

4) Durch Umwandlung von Future in Stream. Es wird selten verwendet.

Möglichkeiten zur Arbeit mit Streams

Nachdem Sie einen Stream auf eine der oben genannten Weisen erstellt haben, müssen Sie lernen, wie Sie dessen Daten verwenden. Flutter bietet zwei Hauptoptionen zur Arbeit mit Streams.

Die erste Möglichkeit ist die Verwendung von Abonnements. Dazu erstellen wir ein Abonnement für einen bestimmten Stream und verwenden es zur Verwaltung von Daten.

Beispiel für ein Abonnement

Abonnements bieten ein umfangreiches Arsenal zur Kontrolle Ihrer Streams. Mit ihrer Hilfe können Sie die Arbeit eines Threads pausieren oder fortsetzen, Funktionen definieren, die bei einem neuen Ereignis (onData), Fehler (onError) oder Abschluss (onDone) ausgelöst werden. Und das Wichtigste: Abonnements ermöglichen es Ihnen, sich mit der cancel() Funktion von einem Stream abzumelden.

Die zweite Option zur Arbeit mit Streams wird vom Flutter SDK bereitgestellt. Es geht um StreamBuilder. Es bietet eine Reihe von Vorteilen: Es bindet die UI an die übertragenen Daten, stellt sicher, dass das Widget mit einem neuen Datenstück neu gezeichnet wird, meldet sich automatisch vom Stream ab, wenn das Widget gelöscht wird, und ermöglicht es Ihnen, Anfangsdaten festzulegen.

Beispiel für einen StreamBuilder

Wir erstellen eine Testliste mit Namen. Die Methode getNameStream() gibt einen Stream zurück, der jede Sekunde einen neuen Namen aus der Liste sendet. Die Klasse StreamBuilderTest überwacht den Zustand des Streams und zeigt ihn als Text an. Jedes Datenstück wird als Text auf dem Bildschirm angezeigt.

Somit bietet Flutter ein reichhaltiges Arsenal zur Erstellung von asynchronem Code unter Verwendung der grundlegenden Funktionen der Dart-Sprache. Das allgemeine Konzept der Arbeit mit Asynchronität ähnelt dem Javascript-Ansatz: ein grundlegender Thread mit einer Ereignisschleife, Promise-ähnliche Futures und die Verwendung der Schlüsselwörter async/await. Um mit einzelnen asynchronen Operationen zu arbeiten, sollten Sie den Future aus dem dart: async-Paket verwenden. Kombinieren Sie ihn mit FutureBuilder aus dem Flutter SDK, um schnell und zuverlässig eine Verbindung zwischen dem Ergebnis einer asynchronen Operation und dem Zustand der UI herzustellen. Streams können verwendet werden, um Ketten asynchroner Ereignisse zu verwalten. Flutter bietet ein ähnliches StreamBuilder-Widget zum Kombinieren von UI und Datenstreams. Zusammen macht dieses Set von asynchronen Werkzeugen Dart und Flutter zu einem hoch effizienten Stack für die Implementierung asynchroner Anwendungen.