Flutter BloC pattern + Provider + tests + souvenez-vous de l'état

Cet article est issu de la publication « Modèle BLoC avec un exemple simple » où nous avons compris ce qu'est ce modèle et comment l'appliquer dans un exemple de compteur simple classique.


Selon les commentaires et pour ma meilleure compréhension, j'ai décidé d'essayer de rédiger une application dans laquelle les réponses aux questions seront reçues:


  1. Comment transférer l'état de la classe dans laquelle se trouve le BloC dans l'application
  2. Comment écrire des tests pour ce modèle
  3. (question supplémentaire) Comment conserver l'état des données entre les lancements d'applications tout en restant dans le modèle BLoC

Ci-dessous est une animashka de l'exemple résultant, et sous la coupe est un débriefing :)


Et à la fin de l'article, un problème intéressant est de savoir comment modifier l'application pour appliquer l' opérateur Debounce à partir du modèle ReactiveX (plus précisément, reactiveX est une extension du modèle Observer)




Description de l'application et du code de base


Sans rapport avec BLoC et fournisseur


  1. L'application a des boutons + - et glissez qui dupliquent ces boutons fonctionnent
  2. Animation réalisée via flutter mixin intégré - TickerProviderStateMixin

Lié à BLoC et au fournisseur


  1. Deux écrans - sur le premier nous glissons, sur le second les changements de compteur sont affichés
  2. Nous écrivons l'état dans le stockage permanent du téléphone (iOS & Android, package https://pub.dev/packages/shared_preferences )
  3. L'écriture et la lecture des informations du stockage persistant sont asynchrones, nous le faisons également via BLoC

Nous rédigeons une candidature


Comme il résulte de la définition du modèle BLoC, notre tâche consiste à supprimer toute la logique des widgets et à travailler avec les données via une classe dans laquelle toutes les entrées et sorties sont des flux.


Dans le même temps, étant donné que la classe dans laquelle se trouve BLoC est utilisée sur différents écrans, nous devons transférer l'objet créé à partir de cette classe dans l'application.


Il existe différentes méthodes pour cela, à savoir:


  1. En passant par les constructeurs de classe, le soi-disant état de levage . Nous ne l'utiliserons pas, car cela s'avère très déroutant, alors ne suivez pas les transferts d'État.
  2. Faites à partir de la classe où nous avons un singleton BLoC et importez-le là où nous en avons besoin. C'est simple et pratique, mais, de mon point de vue purement personnel, complique le constructeur de classe et embrouille un peu la logique.
  3. Utilisez le package Provider - recommandé par l'équipe Flutter pour la gestion des états. Voir la vidéo

Dans cet exemple, nous allons utiliser le fournisseur - pour donner un exemple de toutes les méthodes qui n'avaient pas assez de force :)


Structure générale


Nous avons donc une classe


class SwipesBloc { // some stuff } 

et pour qu'un objet créé à partir de cette classe soit accessible dans toute l'arborescence des widgets, nous, à un certain niveau de widgets d'application, définissons un fournisseur de cette classe. Je l'ai fait tout en haut de l'arborescence des widgets, mais il est préférable de le faire au niveau le plus bas possible.


 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( providers: [ Provider<SwipesBloc>(create: (_) => SwipesBloc()), ], child: MaterialApp( title: 'Swipe BLoC + Provider', 

Après avoir ajouté ce beau design à n'importe quel widget sous l'arbre, un objet avec toutes les données est à notre disposition. Détaille comment travailler avec le fournisseur ici et ici .


Ensuite, nous devons nous assurer que lorsque vous cliquez sur le bouton ou glissez toutes les données sont transférées vers Stream, puis, sur tous les écrans, les données sont mises à jour à partir du même Stream.


Classe pour BLoC


Pour ce faire, nous créons une classe BLoC dans laquelle nous décrivons non seulement les flux, mais également la réception et l'enregistrement de l'état de la mémoire permanente du téléphone.


 import 'dart:async'; import 'package:rxdart/rxdart.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SwipesBloc { Future<SharedPreferences> prefs = SharedPreferences.getInstance(); int _counter; SwipesBloc() { prefs.then((val) { if (val.get('count') != null) { _counter = val.getInt('count') ?? 1; } else { _counter = 1; } _actionController.stream.listen(_changeStream); _addValue.add(_counter); }); } final _counterStream = BehaviorSubject<int>.seeded(1); Stream get pressedCount => _counterStream.stream; void get resetCount => _actionController.sink.add(null); Sink get _addValue => _counterStream.sink; StreamController _actionController = StreamController(); StreamSink get incrementCounter => _actionController.sink; void _changeStream(data) async { if (data == null) { _counter = 1; } else { _counter = _counter + data; } _addValue.add(_counter); prefs.then((val) { val.setInt('count', _counter); }); } void dispose() { _counterStream.close(); _actionController.close(); } } 

Si nous regardons attentivement cette classe, nous verrons que:


  1. Toutes les propriétés disponibles en externe sont les entrées et sorties des flux.
  2. Lors de la première exécution dans le concepteur, nous essayons d'obtenir des données de la mémoire permanente du téléphone.
  3. Enregistré de manière pratique dans la mémoire permanente du téléphone

Petites tâches pour une meilleure compréhension:


  • Prendre un morceau de code de .then du constructeur est préférable de faire une méthode séparée.
  • Essayez d'implémenter cette classe sans fournisseur en tant que Singleton

Recevoir et transmettre des données dans l'application


Maintenant, nous devons transférer des données vers Stream lorsque vous cliquez sur des boutons ou glissez et obtenez ces données sur la carte et sur un écran séparé.


Il existe différentes options pour ce faire, j'ai choisi la classique, nous enveloppons les parties de l'arborescence où vous devez recevoir / transférer des données vers le consommateur


 return Scaffold( body: Consumer<SwipesBloc>( builder: (context, _swipesBloc, child) { return StreamBuilder<int>( stream: _swipesBloc.pressedCount, builder: (context, snapshot) { String counterValue = snapshot.data.toString(); return Stack( children: <Widget>[ Container( 

Eh bien, puis obtenir des données
_swipesBloc.pressedCount,


Transfert de données
_swipesBloc.incrementCounter.add(1);


C'est tout, nous avons un code clair et extensible dans les règles du modèle BLoC.


Exemple de travail


Les tests


Vous pouvez tester des widgets, vous pouvez faire des mokas, vous pouvez e2e.


Nous allons tester les widgets et exécuter l'application en vérifiant le fonctionnement de l'augmentation du compteur. Informations sur les tests ici et ici .


Test de widgets


Si nous avions des données synchrones, nous pourrions tout tester avec des widgets. Dans notre cas, nous ne pouvons que vérifier comment les widgets ont été créés et comment s'est déroulée l'initialisation.


Le code est ici , dans le code il y a des tentatives pour vérifier l'augmentation du compteur après avoir cliqué - cela donne une erreur, car les données passent par BLoC.


Pour exécuter le test, utilisez la commande
flutter test


Tests d'intégration


Dans cette option de test, l'application s'exécute sur l'émulateur, nous pouvons appuyer sur les boutons, balayer et vérifier ce qui s'est passé en conséquence.


Pour ce faire, nous créons 2 fichiers:


test_driver / app.dart
test_driver / app_test.dart


Dans le premier, nous connectons ce qui est nécessaire, et dans le second, directement les tests. Par exemple, j'ai fait les vérifications:


  • État initial
  • Compteur incrémente après avoir appuyé sur un bouton

 import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; void main() { group( 'park-flutter app', () { final counterTextFinder = find.byValueKey('counterKey'); final buttonFinder = find.byValueKey('incrementPlusButton'); FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) { driver.close(); } }); test('test init value', () async { expect(await driver.getText(counterTextFinder), ^_^quot quot^_^); }); test('test + 1 value after tapped', () async { await driver.tap(buttonFinder); // Then, verify the counter text is incremented by 1. expect(await driver.getText(counterTextFinder), ^_^quot quot^_^); }); }, ); } 

Code là


Pour exécuter le test, utilisez la commande
flutter drive --target=test_driver/app.dart


Défi


Juste pour approfondir votre compréhension. Dans les applications modernes (sites), la fonction Debounce de ReactiveX est souvent utilisée.


Par exemple:


  1. Un mot est entré dans la barre de recherche et un indice ne disparaît que lorsque l'écart entre le jeu de lettres est supérieur à 2 secondes
  2. Lorsque des likes sont mis, vous pouvez cliquer 10 fois par seconde - l'écriture dans la base de données se produira si l'écart dans les clics était supérieur à 2-3 secondes
  3. ... etc.

Tâche: effectuer le changement de chiffre uniquement si plus de 2 secondes s'écoulent entre les pressions de + ou -. Pour ce faire, modifiez uniquement la classe BLoC, le reste du code doit rester le même.




C’est tout. Si quelque chose est tordu ou mal, corrigez ici ou sur github , essayez d'atteindre l'idéal :)


Bon codage pour tout le monde!

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


All Articles