Voici la deuxième partie de ma série sur Flutter Architecture:
Les flux sont le principal élément constitutif de RxVMS , leur compréhension est absolument nécessaire pour travailler avec cette bibliothèque, nous allons donc nous attarder sur eux plus en détail dans cet article.
Il s'est avéré que l'inclusion de Rx dans ce post le rendrait trop long, donc je l'ai divisé en deux parties.
Laisse couler
J'ai lu beaucoup de commentaires qui disent que les flux, et en particulier Rx, sont trop compliqués à comprendre et, par conséquent, à utiliser.
J'aimerais que vous sachiez que je ne me considère pas comme un gourou de Rx. Exploiter tout son pouvoir n'est pas facile, et j'avoue que je continue à étudier. Mais permettez-moi de corriger une erreur dès le début: vous n'avez pas besoin d'être un assistant Rx pour commencer à tirer une tonne d'avantages de l'utilisation des threads et de cette technologie . Je mettrai tout en œuvre pour vous expliquer les flux de la manière la plus accessible.
Quels sont les fils?
À mon avis, la meilleure analogie avec les fils est la bande transporteuse. Vous pouvez mettre quelque chose à une extrémité et ce «quelque chose» sera automatiquement transféré à l'autre. Contrairement au pipeline physique, les threads manipulent les objets de données, les transférant automatiquement depuis le début - mais où? Comme dans le vrai pipeline, s'il n'y a rien qui attrape les données à l'autre bout, elles vont simplement «tomber» et disparaître (ce n'est bien sûr pas tout à fait vrai pour Dart Streams, mais il est préférable de gérer les flux comme si c'était le cas) .

Pour éviter la perte de données, vous pouvez définir un "piège" sur la sortie du flux. De cette façon, vous pouvez capturer des données et effectuer les manipulations nécessaires avec elles chaque fois que des objets de données atteignent la fin du flux.

N'oubliez pas:
- Si le piège n'est pas défini, les données disparaîtront tout simplement pour toujours et il n'y aura aucun moyen de les récupérer (encore une fois, pas exactement avec Dart Streams, mais vous feriez mieux de prétendre que c'est le cas)
- Après avoir envoyé des données au flux, vous n'avez pas besoin de suspendre le programme et d'attendre qu'il atteigne la fin, tout cela se passe en arrière-plan.
- Le piège peut recevoir des données à tout moment, ce n'est pas nécessaire immédiatement après l'envoi (mais ne vous inquiétez pas, les flux sont en fait très rapides). Imaginez que vous ne savez pas à quelle vitesse ou combien de temps la bande transporteuse se déplace. Cela signifie que placer quelque chose dans le flux est complètement séparé de la réaction à l'élément à l'autre extrémité. Votre piège fonctionnera et attrapera l'article quand il y arrivera. (Certains d'entre vous réalisent peut-être déjà que cela correspond bien à la façon réactive dont Flutter met à jour ses widgets)
- Vous pouvez définir un piège bien avant le début du travail et le premier élément apparaît
- Le flux fonctionne sur le principe FIFO. Les données viennent toujours dans l'ordre dans lequel elles sont placées dans le flux.
Qu'est-ce que Rx?
Rx, abréviation de Reactive Extensions, sont des flux de stéroïdes. Il s'agit d'un concept très similaire à Streams, qui a été inventé pour le framework .Net par l'équipe Microsoft. Étant donné que .Net avait déjà le type Stream, qui est utilisé pour les E / S de fichiers, ils ont appelé les flux Rx Observables et créé de nombreuses fonctions pour manipuler les données qui les traversent. Dart intègre des flux dans sa spécification de langage qui offrent déjà la plupart de ces fonctionnalités, mais pas toutes. C'est pourquoi le package RxDart a été développé; Il est basé sur Dart Streams, mais étend leurs fonctionnalités. Je couvrirai Rx et RxDart dans la prochaine partie de cette série.
Quelques termes
Dart Streams et Rx utilisent une terminologie qui peut sembler effrayante, alors voici la traduction. Vient d'abord le terme Dart, puis Rx.
- Stream / Observable . Il s'agit du "pipeline" décrit précédemment. Le flux peut être converti en observable et partout où le flux est attendu, vous pouvez attribuer un observable. Alors ne vous embrouillez pas si je mélange ces termes en expliquant
- écouter / s'abonner - définir le piège de l' auditeur
- StreamController / Subject . Le côté "gauche" de la bande transporteuse où vous placez les données dans le Stream. Ils diffèrent légèrement dans leurs propriétés et caractéristiques, mais servent le même but.
- Emission d'un élément / de données . Le moment où les données apparaissent à la sortie du "pipeline"
Création de flux
Si vous avez l'intention de continuer à étudier le sujet, veuillez cloner ce projet avec des exemples. J'utiliserai le système de test Dart / Flutter.
Pour créer un flux, vous créez un StreamController
var controller = new StreamController<String>(); controller.add("Item1");
Le type de modèle (dans ce cas, String) transmis lors de la création de StreamController détermine le type d'objets que nous pouvons envoyer au flux. Cela peut être N'IMPORTE QUEL type! Vous pouvez créer un StreamController<List<MyObject>>()
si vous le souhaitez et le flux transférera la feuille entière au lieu d'un seul objet.
Réglage du piège
Si vous avez exécuté le test spécifié, vous ne pouviez rien voir, car rien n'a attrapé notre ligne à la sortie du flux. Maintenant, posez le piège:
var controller = new StreamController<String>(); controller.stream.listen((item) => print(item));
Le piège est maintenant défini à l'aide de la méthode .listen()
. L'enregistrement ressemble à controller.stream.listen
, mais si vous le faites défiler vers l'arrière, comme une sorte d'album des années 60, alors le vrai sens de ce qui est écrit apparaîtra: "écouter le flux de ce contrôleur"
Vous devez passer une certaine fonction à la méthode .listen()
afin de manipuler en quelque sorte les données entrantes. La fonction doit accepter un paramètre du type spécifié lors de la création du StreamController, dans ce cas, String.
Si vous exécutez le code ci-dessus, vous verrez
Item1 Item2 Item3
À mon avis, le plus gros problème pour les nouveaux arrivants dans Streams est que vous pouvez déterminer la réaction de l'élément émis bien avant que le premier élément ne soit mis dans le flux, déclenchant un appel à cette réaction.
Fin de l'écoute
Le code ci-dessus a manqué la petite mais importante partie. listen()
renvoie un StreamSubscription
- un objet d'abonnement à un flux. Un appel à sa méthode .cancel()
met fin à l'abonnement, libérant des ressources et empêchant votre fonction d'écoute d'être appelée lorsqu'elle devient inutile.
var controller = new StreamController<String>(); StreamSubscription subscription = controller.stream.listen((item) => print(item));
Détails de l'auditeur
La fonction pour listen()
peut être soit une fonction lambda soit une simple fonction.
void myPrint(String message) { print(message); } StreamSubscription subscription = controller.stream.listen((item) => print(item));
Remarque importante: la plupart des flux Dart n'autorisent qu'un abonnement unique, c'est-à-dire qu'ils ne peuvent pas être réinscrits une fois l'abonnement terminé - cela lèvera une exception. C'est leur différence par rapport aux autres implémentations Rx.
La signature complète de listen()
ressemble à ceci:
StreamSubscription<T> listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError});
Cela signifie que vous pouvez faire plus que simplement passer un gestionnaire pour les données envoyées. Vous pouvez également avoir un gestionnaire d'erreurs et un autre pour fermer le flux côté contrôleur ( onDone
). Les exceptions onError()
depuis le Stream onError()
si vous le fournissez, sinon elles sont simplement avalées et vous ne saurez jamais que quelque chose s'est mal passé.
Exemple de fil de flottement
Pour faciliter la compréhension des chapitres suivants, j'ai créé une branche de référentiel distincte.
Veuillez la cloner
Comme premier exemple, j'ai pris l'application de comptage bien connue que vous obtenez lors de la création d'un nouveau projet Flutter, et l'ai un peu réorganisé. J'ai ajouté une classe de modèle pour contenir l'état de l'application, qui est essentiellement une contre-valeur:
class Model { int _counter = 0; StreamController _streamController = new StreamController<int>(); Stream<int> get counterUpdates => _streamController.stream; void incrementCounter() { _counter++; _streamController.add(_counter); } }
ici, vous pouvez voir un modèle très typique: au lieu de publier l'intégralité de StreamController, nous publions simplement sa propriété Stream.
Pour rendre le modèle disponible pour l'interface utilisateur, je l'ai fait un champ statique dans l'objet App parce que je ne voulais pas entrer InheritedWidget ou ServiceLocator. Pour un exemple simple, cela va s'en tirer, mais je ne le ferais pas dans cette application!
Ajoutez à main.dart
:
class _MyHomePageState extends State<MyHomePage> { int _counter = 0; StreamSubscription streamSubscription; @override void initState() { streamSubscription = MyApp.model.counterUpdates.listen((newVal) => setState(() { _counter = newVal; })); super.initState(); }
initState()
bon endroit pour définir l'auditeur, et en tant que bons citoyens de Darts, nous initState()
toujours l'abonnement dans dispose()
, non?
Dans l'arborescence des widgets, il suffit d'adapter le gestionnaire onPressed du bouton FAB (bouton avec une action flottante).
floatingActionButton: new FloatingActionButton( onPressed: MyApp.model.incrementCounter, tooltip: 'Increment', child: new Icon(Icons.add), ),
De cette façon, nous avons créé une séparation nette entre la vue et le modèle à l'aide de Stream.
Appliquer StreamBuilder
Source
Au lieu d'utiliser initState()
et setState()
pour nos besoins, Flutter est livré avec un widget StreamBuilder
pratique. Comme vous l'avez peut-être deviné, il faut une fonction Stream et une méthode constructeur qui est appelée chaque fois que Stream renvoie une nouvelle valeur. Et maintenant, nous n'avons pas besoin d'une initialisation et d'une libération explicites:
body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Text( 'You have pushed the button this many times:', ), StreamBuilder<int>( initialData: 0, stream: MyApp.model.counterUpdates, builder: (context, snappShot) { String valueAsString = 'NoData'; if (snappShot != null && snappShot.hasData) { valueAsString = snappShot.data.toString(); } return Text( valueAsString, style: Theme.of(context).textTheme.display1, ); }), ], ), ),
Nous avons presque terminé, je le promets. Voici trois choses que vous devez savoir:
- Le gros avantage de l'utilisation de StreamBuilder par rapport à la première solution est que l'appel de
setState()
dans listen()
réorganise toujours la page entière, tandis que StreamBuilder n'appellera que son builder
- La variable
snapShot
contient les données les plus récentes reçues de Stream. Vérifiez toujours qu'il contient des données valides avant de l'utiliser. Basé sur les principes d'initialisation pendant, StreamBuilder ne peut pas obtenir de valeur pendant la toute première trame. Pour contourner ce initialData
, nous transmettons la valeur de initialData
, qui est utilisée pour le premier assemblage, c'est-à-dire pour la première image de l'écran. Si nous ne transmettons pas initialData
, notre constructeur sera appelé pour la première fois avec des données invalides. Une alternative à l'utilisation de initialData
consiste à renvoyer un widget d'espace réservé si snapShot
pas valide, qui s'affiche jusqu'à ce que nous obtenions des données valides, par exemple:
Dans le prochain article, nous verrons comment convertir les données dans nos flux et le faire à la volée. Un grand merci à Scott Stoll pour la lecture des épreuves et des commentaires importants.