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

... 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.
Chaque fois que vous souhaitez accéder, appelez simplement
RegistrationType object = sl.get<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
.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(
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() {
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, ),
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() {
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;
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;
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
.