Dies ist der vierte Teil meiner Flutter-Architekturreihe:
Obwohl die beiden vorherigen Teile eindeutig nicht mit dem RxVMS-Muster zusammenhängen, waren sie für ein klares Verständnis dieses Ansatzes erforderlich. Nun wenden wir uns den wichtigsten Paketen zu, die Sie benötigen, um RxVMS in Ihrer Anwendung zu verwenden.
GetIt: Schneller ServiceLocator
Wenn Sie sich an ein Diagramm erinnern, das die verschiedenen RxVMS-Elemente in einer Anwendung zeigt ...

... vielleicht fragen Sie sich, wie die verschiedenen Zuordnungen, Manager und Services voneinander erfahren. Noch wichtiger ist, dass Sie sich fragen, wie ein Element auf die Funktionen eines anderen zugreifen kann.
Bei so vielen verschiedenen Ansätzen (wie geerbte Widgets, IoC-Container, DI ...) bevorzuge ich persönlich den Service Locator. In diesem Zusammenhang habe ich einen speziellen Artikel über GetIt - meine Implementierung dieses Ansatzes, aber hier werde ich dieses Thema etwas ansprechen . Im Allgemeinen registrieren Sie Objekte in diesem Dienst einmal und haben dann in der gesamten Anwendung Zugriff darauf. Es ist eine Art Singleton ... aber mit mehr Flexibilität.
Verwenden Sie
Die Verwendung von GetIt ist ziemlich offensichtlich. Zu Beginn der Anwendung registrieren Sie Dienste und / oder Manager, die Sie später verwenden möchten. Rufen Sie in Zukunft einfach die GetIt-Methoden auf, um auf Instanzen registrierter Klassen zuzugreifen.
Eine nette Funktion ist, dass Sie Schnittstellen oder abstrakte Klassen genau wie konkrete Implementierungen registrieren können. Verwenden Sie beim Zugriff auf eine Instanz einfach Schnittstellen / Abstraktionen, um die erforderlichen Implementierungen bei der Registrierung zu ersetzen. Auf diese Weise können Sie den realen Service einfach auf MockService umstellen.
Ein bisschen Übung
Normalerweise initialisiere ich meinen SeviceLocator in einer Datei namens service_locator.dart über eine globale Variable. Somit wird eine globale Variable für das gesamte Projekt erhalten.
Wann immer Sie zugreifen möchten, rufen Sie einfach an
RegistrationType object = sl.get<RegistrationType>();
Extrem wichtiger Hinweis: Bei Verwendung von GetIt wird beim Importieren von Dateien IMMER derselbe Stil verwendet - entweder Pakete (empfohlen) oder relative Pfade, jedoch nicht beide Ansätze gleichzeitig. Dies liegt daran, dass Dart solche Dateien trotz ihrer Identität als unterschiedlich behandelt.
Wenn dies für Sie schwierig ist, besuchen Sie bitte meinen Blog für Details.
Rxcommand
Nachdem wir GetIt jetzt verwenden, um überall auf unsere Objekte zuzugreifen (einschließlich der Benutzeroberfläche), möchte ich beschreiben, wie wir Handlerfunktionen für UI-Ereignisse implementieren können. Der einfachste Weg wäre, Managern Funktionen hinzuzufügen und sie in Widgets aufzurufen:
class SearchManager { void lookUpZip(String zip); }
und dann in der Benutzeroberfläche
TextField(onChanged: sl.get<SearchManager>().lookUpZip,)
Dies würde bei jeder Änderung im TextField
. Aber wie geben wir das Ergebnis an uns weiter? Da wir reaktiv sein möchten, würden wir unserem StreamController
einen StreamController
hinzufügen:
abstract class SearchManager { Stream<String> get nameOfCity; void lookUpZip(String zip); } class SearchManagerImplementation implements SearchManager{ @override Stream<String> get nameOfCity => cityController.stream; StreamController<String> cityController = new StreamController(); @override Future<void> lookUpZip(String zip) async { var cityName = await sl.get<ZipApiService>().lookUpZip(zip); cityController.add(cityName); } }
und in der Benutzeroberfläche:
StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().nameOfCity, builder: (context, snapshot) => Text(snapShot.data);
Obwohl dieser Ansatz funktioniert, ist er nicht optimal. Hier sind die Probleme:
- Redundanter Code - Wir müssen immer eine Methode, StreamController und einen Getter für seinen Stream erstellen, wenn wir ihn nicht explizit im öffentlichen Bereich anzeigen möchten
- Besetztzustand - Was ist, wenn wir Spinner anzeigen möchten, während die Funktion ihre Arbeit erledigt?
- Fehlerbehandlung - Was passiert, wenn eine Funktion eine Ausnahme auslöst?
Natürlich könnten wir weitere StreamController hinzufügen, um Zustände und Fehler zu behandeln ... aber bald wird es langweilig, und das Paket rx_command ist hier nützlich.
RxCommand
löst alle oben genannten Probleme und vieles mehr. RxCommand
kapselt eine Funktion (synchron oder asynchron) und veröffentlicht ihre Ergebnisse automatisch im Stream.
Mit RxCommand können wir unseren Manager folgendermaßen umschreiben:
abstract class SearchManager { RxCommand<String,String> lookUpZipCommand; } class SearchManagerImplementation implements SearchManager{ @override RxCommand<String,String> lookUpZipCommand; SearchManagerImplementation() { lookUpZipCommand = RxCommand.createAsync((zip) => sl.get<ZipApiService>().lookUpZip(zip)); } }
und in der Benutzeroberfläche:
TextField(onChanged: sl.get<SearchManager>().lookUpZipCommand,) ... StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().lookUpZipCommand, builder: (context, snapshot) => Text(snapShot.data);
Das ist viel prägnanter und lesbarer.
RxCommand im Detail

RxCommand hat einen Eingang und fünf Ausgang Observables:
canExecuteInput ist ein optionales Observable<bool>
, das Sie beim Erstellen des RxCommand an die Factory-Funktion übergeben können. Es signalisiert RxCommand, ob es ausgeführt werden kann, abhängig vom zuletzt empfangenen Wert.
isExecuting ist ein Observable<bool>
, der signalisiert, ob der Befehl gerade seine Funktion ausführt. Wenn ein Team beschäftigt ist, kann es nicht erneut ausgeführt werden. Wenn Sie Spinner zur Laufzeit anzeigen möchten, hören Sie isExecuting
canExecute ist ein Observable<bool>
, der die Fähigkeit signalisiert, einen Befehl auszuführen. Dies passt zum Beispiel gut zu StreamBuilder, um das Aussehen einer Schaltfläche zwischen Ein / Aus-Status zu ändern.
seine Bedeutung ist wie folgt:
Observable<bool> canExecute = Observable.combineLatest2<bool,bool>(canExecuteInput,isExecuting) => canExecuteInput && !isExecuting).distinct.
was bedeutet
- wird
false
wenn isExecuting true
zurückgibt - Es wird nur dann true zurückgegeben
true
wenn isExecuting false
zurückgibt UND canExecuteInput nicht false
zurückgibt.
throwExceptions ist eine Observable<Exception>
. Alle Ausnahmen, die eine gepackte Funktion auslösen kann, werden abgefangen und an dieses Observable gesendet. Es ist praktisch, es anzuhören und ein Dialogfeld anzuzeigen, wenn ein Fehler auftritt
(das Team selbst) ist eigentlich auch ein Observable. Die von der Arbeitsfunktion zurückgegebenen Werte werden auf diesem Kanal übertragen, sodass Sie RxCommand als Stream-Parameter direkt an StreamBuilder übergeben können
Die Ergebnisse enthalten alle Befehlszustände in einem Observable<CommandResult>
, in dem CommandResult
definiert ist als
.results
Observable ist besonders nützlich, wenn Sie das Ergebnis eines Befehls direkt an StreamBuilder übergeben möchten. Dies zeigt je nach Status des Befehls unterschiedliche Inhalte an und funktioniert sehr gut mit RxLoader
aus dem Paket rx_widgets . Hier ist ein Beispiel für ein RxLoader-Widget, das .results
Observable verwendet:
Expanded(
RxCommands erstellen
RxCommands können synchrone und asynchrone Funktionen verwenden, die:
- Sie haben keinen Parameter und geben kein Ergebnis zurück.
- Sie haben einen Parameter und geben kein Ergebnis zurück.
- Sie haben keinen Parameter und geben ein Ergebnis zurück.
- Sie haben einen Parameter und geben ein Ergebnis zurück.
Für alle Optionen bietet RxCommand verschiedene Factory-Methoden unter Berücksichtigung synchroner und asynchroner Handler:
static RxCommand<TParam, TResult> createSync<TParam, TResult>(Func1<TParam, TResult> func,... static RxCommand<void, TResult> createSyncNoParam<TResult>(Func<TResult> func,... static RxCommand<TParam, void> createSyncNoResult<TParam>(Action1<TParam> action,... static RxCommand<void, void> createSyncNoParamNoResult(Action action,... static RxCommand<TParam, TResult> createAsync<TParam, TResult>(AsyncFunc1<TParam, TResult> func,... static RxCommand<void, TResult> createAsyncNoParam<TResult>(AsyncFunc<TResult> func,... static RxCommand<TParam, void> createAsyncNoResult<TParam>(AsyncAction1<TParam> action,... static RxCommand<void, void> createAsyncNoParamNoResult(AsyncAction action,...
Auch wenn Ihre gepackte Funktion keinen Wert zurückgibt, gibt RxCommand nach Ausführung der Funktion einen leeren Wert zurück. Auf diese Weise können Sie den Listener auf einen solchen Befehl einstellen, damit er auf den Abschluss der Funktion reagiert.
Zugriff auf das neueste Ergebnis
RxCommand.lastResult
Sie Zugriff auf den letzten erfolgreichen Wert des Ergebnisses der Befehlsausführung, der als initialData
in StreamBuilder verwendet werden kann.
Wenn Sie das letzte Ergebnis zur Laufzeit oder im Fehlerfall in CommandResult-Ereignisse aufnehmen möchten, können emitInitialCommandResult = true
beim Erstellen des Befehls emitInitialCommandResult = true
.
Wenn Sie .lastResult
einen Anfangswert zuweisen .lastResult
, z. B. wenn Sie ihn in StreamBuilder als initialData
verwenden, können Sie initialLastResult
beim Erstellen des Befehls mit dem Parameter initialLastResult
.
Beispiel - Flutter reaktiv machen
Die neueste Version des Beispiels wurde für RxVMS neu organisiert. Daher sollten Sie jetzt eine gute Option für die Verwendung haben.
Da dies eine sehr einfache Anwendung ist, benötigen wir nur einen Manager:
class AppManager { RxCommand<String, List<WeatherEntry>> updateWeatherCommand; RxCommand<bool, bool> switchChangedCommand; RxCommand<String, String> textChangedCommand; AppManager() {
Sie können verschiedene RxCommands miteinander kombinieren. Beachten Sie, dass switchChangedCommand tatsächlich ein Observable canExecute für updateWeatherCommand
.
Nun wollen wir sehen, wie der Manager in der Benutzeroberfläche verwendet wird:
return Scaffold( appBar: AppBar(title: Text("WeatherDemo")), resizeToAvoidBottomPadding: false, body: Column( children: <Widget>[ Padding( padding: const EdgeInsets.all(16.0), child: TextField( key: AppKeys.textField, autocorrect: false, controller: _controller, decoration: InputDecoration( hintText: "Filter cities", ), style: TextStyle( fontSize: 20.0, ),
Typische Verwendungsmuster
Wir haben bereits eine Möglichkeit gesehen, mit CommandResults auf verschiedene Zustände eines Befehls zu reagieren. In Fällen, in denen angezeigt werden soll, ob der Befehl erfolgreich war (das Ergebnis jedoch nicht angezeigt wird), wird häufig der Befehl Observables in der Funktion initState
StatefulWidget initState
. Hier ist ein Beispiel für ein reales Projekt.
Definition für createEventCommand
:
RxCommand<Event, void> createEventCommand;
Dadurch wird ein Ereignisobjekt in der Datenbank erstellt und es wird kein realer Wert zurückgegeben. Wie wir bereits erfahren haben, gibt sogar ein RxCommand mit dem Rückgabetyp void
nach Abschluss ein einzelnes Datenelement zurück. Daher können wir dieses Verhalten verwenden, um eine Aktion in unserer Anwendung auszulösen, sobald der Befehl ausgeführt wird:
@override void initState() {
Wichtig : Vergessen Sie nicht, die Abonnements abzuschließen, wenn wir sie nicht mehr benötigen:
@override void dispose() { _eventCommandSubscription?.cancel(); _errorSubscription?.cancel(); super.dispose(); }
Wenn Sie die Anzeige des Besetztzählers verwenden möchten, können Sie außerdem:
- Hören Sie sich
isExecuting
Observable-Befehle in der initState
Funktion an. - den Zähler im Abonnement ein- / ausblenden; und auch
- Verwenden Sie den Befehl selbst als Datenquelle für StreamBuilder
Mit RxCommandListeners das Leben leichter machen
Wenn Sie mehrere Observable verwenden möchten, müssen Sie wahrscheinlich mehrere Abonnements verwalten. Die direkte Kontrolle über das Abhören und Freigeben einer Gruppe von Abonnements kann schwierig sein, macht den Code weniger lesbar und birgt das Risiko von Fehlern (z. B. das Vergessen, während des Abschlussvorgangs zu cancel
).
Die neueste Version von rx_command fügt die RxCommandListener
RxCommandListener hinzu, die diese Verarbeitung vereinfachen soll. Sein Konstruktor akzeptiert Befehle und Handler für verschiedene Statusänderungen:
class RxCommandListener<TParam, TResult> { final RxCommand<TParam, TResult> command;
Sie müssen nicht alle Handlerfunktionen übergeben. Alle sind optional, sodass Sie einfach die benötigten übertragen können. Sie müssen in Ihrer dispose
nur RxCommandListener
für Ihren RxCommandListener
, und alle im Abonnement verwendeten werden RxCommandListener
.
Vergleichen wir denselben Code mit und ohne RxCommandListener
in einem anderen realen Beispiel. Der Befehl selectAndUploadImageCommand
wird hier auf dem Chat-Bildschirm verwendet, über den der Benutzer Bilder hochladen kann. Wenn der Befehl aufgerufen wird:
- Das ImagePicker- Dialogfeld wird angezeigt.
- Nach Auswahl wird das Bild geladen
- Nach Abschluss des Downloads gibt der Befehl die Bildspeicheradresse zurück, damit Sie einen neuen Chat-Eintrag erstellen können.
Ohne RxCommandListener :
_selectImageCommandSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .listen((imageLocation) async { if (imageLocation == null) return;
Verwenden von RxCommandListener :
selectImageListener = RxCommandListener( command: sl.get<ImageManager>().selectAndUploadImageCommand, onValue: (imageLocation) async { if (imageLocation == null) return; sl.get<EventManager>().createChatEntryCommand(new ChatEntry( event: widget.event, isImage: true, content: imageLocation.downloadUrl, )); }, onIsBusy: () => MySpinner.show(context), onNotBusy: MySpinner.hide, onError: (ex) => showMessageDialog(context, 'Upload problem', "We cannot upload your selected image at the moment. Please check your internet connection"));
Im Allgemeinen würde ich immer einen RxCommandListener verwenden, wenn mehr als ein Observable vorhanden ist.
Probieren Sie RxCommands aus und sehen Sie, wie es Ihnen das Leben erleichtern kann.
Übrigens müssen Sie RxVMS nicht verwenden, um RxCommands nutzen zu können .
Weitere Informationen zu RxCommand
Sie im Readme- Paket RxCommand
.