RxVMS-Grundlagen: RxCommand und GetIt

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


Bild


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


//    GetIt sl = new GetIt(); void setUpServiceLocator(ErrorReporter reporter) { //  // [registerSingleton]  -  . //   . // sl.get<ErrorReporter>.get()    . sl.registerSingleton<ErrorReporter>(reporter); // [registerLazySingleton]   ,       // sl.get<ImageService>.get()            . sl.registerLazySingleton<ImageService>(() => new ImageServiceImplementation()); sl.registerLazySingleton<MapService>(() => new MapServiceImplementation()); //  sl.registerSingleton<UserManager>(new UserManagerImplementation()); sl.registerLazySingleton<EventManager>(() => new EvenManagerImplementation()); sl.registerLazySingleton<ImageManager>(() => new ImageManagerImplementation()); sl.registerLazySingleton<AppManager>(() => new AppManagerImplementation()); 

Wann immer Sie zugreifen möchten, rufen Sie einfach an


 RegistrationType object = sl.get<RegistrationType>(); //  GetIt  `callable`,   : RegistrationType object2 = sl<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



 /// Combined execution state of an `RxCommand` /// Will be issued for any state change of any of the fields /// During normal command execution, you will get this item's listening at the command's [.results] observable. /// 1. If the command was just newly created, you will get `null, false, false` (data, error, isExecuting) /// 2. When calling execute: `null, false, true` /// 3. When exceution finishes: `result, false, false` class CommandResult<T> { final T data; final Exception error; final bool isExecuting; const CommandResult(this.data, this.error, this.isExecuting); bool get hasData => data != null; bool get hasError => error != null; @override bool operator ==(Object other) => other is CommandResult<T> && other.data == data && other.error == error && other.isExecuting == isExecuting; @override int get hashCode => hash3(data.hashCode, error.hashCode, isExecuting.hashCode); @override String toString() { return 'Data: $data - HasError: $hasError - IsExecuting: $isExecuting'; } } 

.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( /// RxLoader      ///    Stream<CommandResult> child: RxLoader<List<WeatherEntry>>( spinnerKey: AppKeys.loadingSpinner, radius: 25.0, commandResults: sl.get<AppManager>().updateWeatherCommand.results, /// ,  .hasData == true dataBuilder: (context, data) => WeatherListView(data, key: AppKeys.weatherList), /// ,  .isExceuting == true placeHolderBuilder: (context) => Center( key: AppKeys.loaderPlaceHolder, child: Text("No Data")), /// ,  .hasError == true errorBuilder: (context, ex) => Center( key: AppKeys.loaderError, child: Text("Error: ${ex.toString()}")), ), ), 

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() { //    bool         //   Observable switchChangedCommand = RxCommand.createSync<bool, bool>((b) => b); //    switchChangedCommand  canExecute Observable  // updateWeatherCommand updateWeatherCommand = RxCommand.createAsync<String, List<WeatherEntry>>( sl.get<WeatherService>().getWeatherEntriesForCity, canExecute: switchChangedCommand, ); //         textChangedCommand = RxCommand.createSync<String, String>((s) => s); //    ... textChangedCommand //     500ms... .debounce(new Duration(milliseconds: 500)) // ...   updateWeatherCommand .listen(updateWeatherCommand); //     updateWeatherCommand(''); } } 

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, ), //    textChangedCommand! onChanged: sl.get<AppManager>().textChangedCommand, ), ), Expanded( /// RxLoader   builders   ///   Stream<CommandResult> child: RxLoader<List<WeatherEntry>>( spinnerKey: AppKeys.loadingSpinner, radius: 25.0, commandResults: sl.get<AppManager>().updateWeatherCommand.results, dataBuilder: (context, data) => WeatherListView(data, key: AppKeys.weatherList), placeHolderBuilder: (context) => Center(key: AppKeys.loaderPlaceHolder, child: Text("No Data")), errorBuilder: (context, ex) => Center(key: AppKeys.loaderError, child: Text("Error: ${ex.toString()}")), ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: <Widget>[ ///  Updatebutton    updateWeatherCommand.canExecute Expanded( //        Streambuilder, //      WidgetSelector child: WidgetSelector( buildEvents: sl .get<AppManager>() .updateWeatherCommand .canExecute, onTrue: RaisedButton( key: AppKeys.updateButtonEnabled, child: Text("Update"), onPressed: () { _controller.clear(); sl.get<AppManager>().updateWeatherCommand(); }, ), onFalse: RaisedButton( key: AppKeys.updateButtonDisabled, child: Text("Please Wait"), onPressed: null, ), ), ), //    canExecuteInput StateFullSwitch( state: true, onChanged: sl.get<AppManager>().switchChangedCommand, ), ], ), ), ], ), ); 

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() { //      ,        _eventCommandSubscription = _createCommand.listen((_) async { Navigator.pop(context); await showToast('Event saved'); }); //        _errorSubscription = _createEventCommand.thrownExceptions.listen((ex) async { await sl.get<ErrorReporter>().logException(ex); await showMessageDialog(context, 'There was a problem saving event', ex.toString()); }); } 

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; //       final void Function(TResult value) onValue; //    isExecuting final void Function(bool isBusy) onIsBusyChange; //       final void Function(Exception ex) onError; //    canExecute final void Function(bool state) onCanExecuteChange; //    .results Observable  final void Function(CommandResult<TResult> result) onResult; //     /  final void Function() onIsBusy; final void Function() onNotBusy; //     final Duration debounceDuration; RxCommandListener(this.command,{ this.onValue, this.onIsBusyChange, this.onIsBusy, this.onNotBusy, this.onError, this.onCanExecuteChange, this.onResult, this.debounceDuration,} ) void dispose(); 

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; //    sl.get<EventManager>().createChatEntryCommand(new ChatEntry( event: widget.event, isImage: true, content: imageLocation.downloadUrl, )); }); _selectImageIsExecutingSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .isExecuting .listen((busy) { if (busy) { MySpinner.show(context); } else { MySpinner.hide(); } }); _selectImageErrorSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .thrownExceptions .listen((ex) => showMessageDialog(context, 'Upload problem', "We cannot upload your selected image at the moment. Please check your internet connection")); 

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 .

Source: https://habr.com/ru/post/de449872/


All Articles