Principes de base de RxVMS: RxCommand et GetIt

Ceci est la quatrième partie de ma série d'architecture Flutter:



Bien que les 2 parties précédentes n'étaient clairement pas liées au modèle RxVMS, elles étaient nécessaires pour une compréhension claire de cette approche. Nous passons maintenant aux packages les plus importants dont vous aurez besoin pour utiliser RxVMS dans votre application.


GetIt: Fast ServiceLocator


Lorsque vous vous souvenez d'un diagramme montrant les différents éléments RxVMS dans une application ...


image


... vous vous demandez peut-être comment les différents mappings, gestionnaires et services se découvrent mutuellement. Plus important encore, vous vous demandez peut-être comment un élément peut accéder aux fonctions d'un autre.


Avec autant d'approches différentes (telles que les widgets hérités, les conteneurs IoC, DI ...), je préfère personnellement le Service Locator. À cet égard, j'ai un article spécial sur GetIt - ma mise en œuvre de cette approche, mais ici je vais aborder ce sujet légèrement. En général, vous enregistrez des objets dans ce service une fois, puis vous y avez accès tout au long de l'application. C'est une sorte de singleton ... mais avec plus de flexibilité.


Utiliser


Utiliser GetIt est assez évident. Au tout début de l'application, vous enregistrez les services et / ou managers que vous prévoyez d'utiliser ultérieurement. À l'avenir, il suffit d'appeler les méthodes GetIt pour accéder aux instances des classes enregistrées.


Une fonctionnalité intéressante est que vous pouvez enregistrer des interfaces ou des classes abstraites comme des implémentations concrètes. Lorsque vous accédez à une instance, utilisez simplement des interfaces / abstractions, remplaçant facilement les implémentations nécessaires lors de l'enregistrement. Cela vous permet de basculer facilement le vrai service vers MockService.


Un peu de pratique


J'initialise généralement mon SeviceLocator dans un fichier appelé service_locator.dart via une variable globale. Ainsi, une variable globale est obtenue pour l'ensemble du projet.


//    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()); 

Chaque fois que vous souhaitez accéder, appelez simplement


 RegistrationType object = sl.get<RegistrationType>(); //  GetIt  `callable`,   : RegistrationType object2 = sl<RegistrationType>(); 

Remarque extrêmement importante: lors de l'utilisation de GetIt, TOUJOURS utiliser le même style lors de l'importation de fichiers - packages (recommandé) ou chemins relatifs, mais pas les deux approches à la fois. En effet, Dart traite ces fichiers comme différents, malgré leur identité.


Si cela vous est difficile, veuillez consulter mon blog pour plus de détails.


Rxcommand


Maintenant que nous utilisons GetIt pour accéder à nos objets partout (y compris l'interface utilisateur), je veux décrire comment nous pouvons implémenter des fonctions de gestionnaire pour les événements d'interface utilisateur. Le moyen le plus simple serait d'ajouter des fonctions aux gestionnaires et de les appeler dans des widgets:


 class SearchManager { void lookUpZip(String zip); } 

puis dans l'interface utilisateur


 TextField(onChanged: sl.get<SearchManager>().lookUpZip,) 

Cela lookUpZip à chaque changement dans le TextField . Mais comment nous transmettre le résultat à l'avenir? Puisque nous voulons être réactifs, nous ajouterions un StreamController à notre SearchManager :


 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); } } 

et dans l'interface utilisateur:


 StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().nameOfCity, builder: (context, snapshot) => Text(snapShot.data); 

Bien que cette approche fonctionne, elle n'est pas optimale. Voici les problèmes:


  • code redondant - nous devons toujours créer une méthode, StreamController et un getter pour son flux, si nous ne voulons pas l'afficher explicitement dans le domaine public
  • état occupé - que se passe-t-il si nous souhaitons afficher Spinner pendant que la fonction fait son travail?
  • gestion des erreurs - que se passe-t-il si une fonction lève une exception?

Bien sûr, nous pourrions ajouter plus de StreamControllers pour gérer les états et les erreurs ... mais bientôt cela devient fastidieux, et le package rx_command est utile ici .


RxCommand résout tous les problèmes ci-dessus et bien plus encore. RxCommand encapsule une fonction (synchrone ou asynchrone) et publie automatiquement ses résultats dans le flux.


En utilisant RxCommand, nous pourrions réécrire notre gestionnaire comme ceci:


 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)); } } 

et dans l'interface utilisateur:


 TextField(onChanged: sl.get<SearchManager>().lookUpZipCommand,) ... StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().lookUpZipCommand, builder: (context, snapshot) => Text(snapShot.data); 

ce qui est beaucoup plus concis et lisible.


RxCommand en détail



RxCommand a une entrée et cinq sorties observables:


  • canExecuteInput est un Observable<bool> facultatif que vous pouvez transmettre à la fonction d'usine lors de la création de RxCommand. Il signale à RxCommand si elle peut être exécutée, selon la dernière valeur reçue.


  • isExecuting est un Observable<bool> qui signale si la commande exécute actuellement sa fonction. Lorsqu'une équipe est occupée, elle ne peut pas être réexécutée. Si vous souhaitez afficher Spinner à l'exécution, écoutez isExecuting


  • canExecute est un Observable<bool> qui signale la possibilité d'exécuter une commande. Cela, par exemple, va bien avec StreamBuilder pour changer l'apparence d'un bouton entre l'état activé / désactivé.
    sa signification est la suivante:


     Observable<bool> canExecute = Observable.combineLatest2<bool,bool>(canExecuteInput,isExecuting) => canExecuteInput && !isExecuting).distinct. 

    ce qui signifie


    • sera false si isExecuting renvoie true
    • il ne sera renvoyé true que si isExecuting renvoie false ET canExecuteInput ne retourne pas false .

  • thrownExceptions est une Observable<Exception> . Toutes les exceptions qu'une fonction empaquetée peut lever seront interceptées et envoyées à cet observable. Il est pratique de l'écouter et d'afficher une boîte de dialogue en cas d'erreur.


  • (l'équipe elle-même) est également une observable. Les valeurs retournées par la fonction de travail seront transmises sur ce canal, vous pouvez donc passer directement RxCommand à StreamBuilder en tant que paramètre de flux


  • les résultats contiennent tous les états de commande dans un Observable<CommandResult> , où CommandResult défini comme



 /// 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 .results est particulièrement utile si vous souhaitez transmettre le résultat d'une commande directement à StreamBuilder. Cela affichera un contenu différent selon l'état de la commande, et cela fonctionne très bien avec RxLoader du package rx_widgets . Voici un exemple de widget RxLoader qui utilise .results Observable:


 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()}")), ), ), 

Création de RxCommands


RxCommands peut utiliser des fonctions synchrones et asynchrones qui:


  • Ils n'ont pas de paramètre et ne renvoient pas de résultat;
  • Ils ont un paramètre et ne renvoient pas de résultat;
  • Ils n'ont pas de paramètre et renvoient un résultat;
  • Ils ont un paramètre et retournent un résultat;

Pour toutes les options, RxCommand propose plusieurs méthodes d'usine, prenant en compte les gestionnaires synchrones et asynchrones:


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

Même si votre fonction compressée ne renvoie pas de valeur, RxCommand renvoie une valeur vide après l'exécution de la fonction. Ainsi, vous pouvez définir l'écouteur sur une telle commande afin qu'il réponde à l'achèvement de la fonction.


Accès au dernier résultat


RxCommand.lastResult vous donne accès à la dernière valeur réussie du résultat de l'exécution de la commande, qui peut être utilisée comme initialData dans StreamBuilder.


Si vous souhaitez obtenir le dernier résultat inclus dans les événements CommandResult au moment de l'exécution ou en cas d'erreur, vous pouvez transmettre emitInitialCommandResult = true lors de la création de la commande.


Si vous souhaitez affecter une valeur initiale à .lastResult , par exemple, si vous l'utilisez comme initialData dans StreamBuilder, vous pouvez la transmettre avec le paramètre initialLastResult lors de la création de la commande.


Exemple - rendre Flutter réactif


La dernière version de l'exemple a été réorganisée pour RxVMS , vous devriez donc maintenant avoir une bonne option sur la façon de l'utiliser.


Comme il s'agit d'une application très simple, nous n'avons besoin que d'un seul gestionnaire:


 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(''); } } 

Vous pouvez combiner différents RxCommands ensemble. Notez que SwitchChangedCommand est en fait un canExecute observable pour updateWeatherCommand .


Voyons maintenant comment le Manager est utilisé dans l'interface utilisateur:


  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, ), ], ), ), ], ), ); 

Modèles d'utilisation typiques


Nous avons déjà vu une façon de répondre aux différents états d'une commande en utilisant CommandResults . Dans les cas où nous voulons afficher si la commande a réussi (mais pas afficher le résultat), un modèle courant consiste à écouter la commande Observables dans la fonction initState StatefulWidget. Voici un exemple de vrai projet.


Définition de createEventCommand :


  RxCommand<Event, void> createEventCommand; 

Cela créera un objet Event dans la base de données et ne retournera aucune valeur réelle. Mais, comme nous l'avons appris précédemment, même un RxCommand avec un type de retour de void retournera un seul élément de données à la fin. Ainsi, nous pouvons utiliser ce comportement pour déclencher une action dans notre application dès que la commande est terminée:


 @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()); }); } 

Important : n'oubliez pas de compléter les abonnements lorsque nous n'en avons plus besoin:


 @override void dispose() { _eventCommandSubscription?.cancel(); _errorSubscription?.cancel(); super.dispose(); } 

De plus, si vous souhaitez utiliser l'affichage du compteur occupé, vous pouvez:


  • écouter les commandes isExecuting Observable dans la fonction initState ;
  • afficher / masquer le compteur dans l'abonnement; aussi
  • utiliser la commande elle-même comme source de données pour StreamBuilder

Rendre la vie plus facile avec RxCommandListeners


Si vous souhaitez utiliser plusieurs observables, vous devrez probablement gérer plusieurs abonnements. Le contrôle direct de l'écoute et de la libération d'un groupe d'abonnements peut être difficile, rend le code moins lisible et vous expose à des erreurs (par exemple, oublier d' cancel en cours de finalisation).


La dernière version de rx_command ajoute la classe d'assistance RxCommandListener , qui est conçue pour simplifier ce traitement. Son constructeur accepte les commandes et les gestionnaires pour divers changements d'état:


 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(); 

Vous n'avez pas besoin de passer toutes les fonctions du gestionnaire. Tous sont facultatifs, vous pouvez donc simplement transférer ceux dont vous avez besoin. Vous avez seulement besoin d'appeler dispose pour votre RxCommandListener dans votre fonction dispose , et il annulera tout utilisé dans l'abonnement.


Comparons le même code avec et sans RxCommandListener dans un autre exemple réel. La commande selectAndUploadImageCommand est utilisée ici sur l'écran de discussion où l'utilisateur peut télécharger des images. Lorsque la commande est appelée:


  • La boîte de dialogue ImagePicker s'affiche.
  • Après avoir sélectionné l'image est chargée
  • Une fois le téléchargement terminé, la commande renvoie l'adresse de stockage d'image afin que vous puissiez créer une nouvelle entrée de conversation.

Sans 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")); 

Utilisation de 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")); 

Généralement, j'utiliserais toujours un RxCommandListener s'il y a plus d'un observable.


Essayez RxCommands et voyez comment cela peut vous faciliter la vie.
Soit dit en passant, vous n'avez pas besoin d'utiliser RxVMS pour profiter des RxCommands .


Pour plus d'informations sur RxCommand lisez le RxCommand readme de RxCommand .

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


All Articles