State management avec BLoC en Flutter

Quand on commence à développer une application, une des questions principales qu’on se pose c’est : comment architecturer le projet pour le rendre testable, modulable et flexible ? Dans notre précédent article sur le démarrage d’un projet Flutter, nous avions déjà parlé des patterns existants sous un environnement Flutter.
Nous allons aujourd’hui voir comment implémenter le BLoC, l’un des patterns de state management en Flutter les plus connus.
Si vous avez déjà eu l’occasion de créer une application en Flutter, vous connaissez peut-être le Counter App. Il s’agit d’une application d’exemple fournie avec Flutter et destinée à fournir un modèle de base et illustrer certains avantages de Flutter tels que HotReload ou HotRestart.
Cette application d’exemple est assez simple : elle reprend une page sur laquelle se trouvent un bouton et un texte indiquant le nombre de fois où l’on appuie sur ce bouton (d’où le nom de « Counter App » donné à cette application).
Dans cet article, nous allons transformer étape par étape l’application d’exemple Counter en utilisant le design pattern BLoC et ses composantes.
Design pattern BLoC
L’architecture d’une application est souvent l’un des sujets les plus débattus lors du développement d’applications. En effet, chacun semble avoir son modèle architectural préféré.
Les développeurs d’applications mobiles séparent le projet sur plusieurs couches pour mieux dispatcher les responsabilités entre les modules. Le modèle et la vue sont séparés, le contrôleur (ou ViewModel) envoyant des évènements entre eux. Ces approches, bien connues des développeurs natifs et Xamarin, sont : Model-View-Controller (MVC) ou Model-View-ViewModel (MVVM). Une variante de ce modèle classique a émergé de la communauté Flutter : BLoC.
BLoC signifie Business Logic Components. L’essentiel de BLoC est que tout, dans l’application, doit être représenté comme un flux d’événements. Le widget reproduit des évènements : interaction avec l’interface graphique, évènement système, changement d’orientation, etc. Le BLoC se trouve derrière le widget et il a la charge d’écouter et de réagir aux évènements produits par le widget. L’objectif de BLoC est en quelques sorte de gérer le cycle de vie d’un widget. Le langage de programmation Dart est même livré avec une syntaxe pour travailler avec des flux intégrés dans le langage. On peut donc considérer le BLoC comme étant une architecture clé en utilisant Flutter dès lors que l’on souhaite piloter un widget contenant de la logique métier.
Cliquer sur l’image pour lancer l’animation
Ajoutons un fichier home_bloc.dart dans le projet. Ce fichier représentant notre BLoC va contenir la valeur et la logique d’augmentation du compteur.
[home_bloc.dart]
L’interaction avec le widget se fait par le StreamController : il s’agit d’un objet du framework Dart permettant la gestion de streams. Ce composant permet de transmettre les données, les événements, ainsi que tout type d’objets, d’un endroit à l’autre.
Dans HomeBloc, nous ajoutons deux stream controllers :
- l’un pour transmettre l’événement d’augmentation de compteur du widget vers le BLoC ;
- l’autre pour transmettre la valeur actuelle du compteur au widget.
Il faut considérer le StreamController comme un pipe. Chaque pipe contient deux parties : l’une est branchée à la source des données et l’autre est branchée au consommateur.
Le côté branché à la source en Flutter s’appelle Sink. Celui branché au consommateur se nomme Stream.
Cliquer sur l’image pour lancer l’animation
Dans HomeBloc, on a deux StreamController :
- _counterInStreamController : pour transmettre la valeur du compteur à la vue
- _counterOutStreamController : pour envoyer l’évènement d’incrément au compteur.
Dans le constructeur de HomeBloc, je m’abonne au stream de _counterInStreamController pour gérer les évènements de la vue.
Si je reçois IncreaseEvent dans la handler de mon stream, il faut augmenter le counter et le renvoyer à la vue en appelant _counterOutStreamController.sink.add.
Ainsi, mon HomeBloc expose counterInSink et counterOutStream pour que la vue puisse interagir avec le BLoC.
Notre HomeBloc est prêt à être consommé par le widget. Nous pourrions créer l’instance de BLoC directement dans le widget, mais au lieu de procéder ainsi, je vous propose de parler d’IoC en Flutter.
IoC en Flutter
Contrairement à l’écosystème .Net, en Flutter les réflexions sont interdites par design en raison du coût possible sur les performances de l’application. Mais cela ne signifie pas que l’IoC et l’injection des dépendances ne sont pas possibles en Flutter.
Dans ce framework, le moyen par défaut de fournir des objets / services aux widgets se fait via InheritedWidgets. Si vous souhaitez qu’un widget ou son modèle ait accès à un service, le widget doit être un enfant du widget hérité. Cela provoque une imbrication inutile.
C’est là que l’injection des dépendances entre en jeu. Un IoC vous permet d’enregistrer vos types de cours et de faire la demande depuis n’importe où dès lors que vous avez accès au conteneur. Get_it, le package maintenu par la communauté Flutter, garantit ainsi une certaine stabilité.
Pour ajouter le package dans notre projet il faut ajouter la ligne suivant dans pubspec.yaml :
L’étape suivante consiste à configurer le container avec les dépendances que nous allons utiliser dans le projet. Dans la fonction Main, avant de créer l’application, je configure l’IoC.
[Main.dart]
L’instance de container est statique et pour y accéder, il suffit d’appeler GetIt.instance depuis l’endroit où c’est nécessaire. Ici, dans registerServices, j’enregistre mon HomeBloc en tant que factory : le container pourra donc créer une instance de BLoC à la demande.
Dans l’exemple ci-dessous, j’enregistre mes injections dans la méthode main juste avant de lancer l’application :
Maintenant que nous avons configuré le container et BLoC, nous pouvons passer à l’étape de consommation et création de widget.
StateFull Widget et BLoC
Tout est un widget en Flutter. Chaque widget permet de décrire ce à quoi sa vue doit ressembler en fonction de sa configuration et de son état. Lorsque l’état d’un widget change, le framework calcule la différence avec l’état précédent afin de déterminer les modifications minimales nécessaires dans l’arbre de rendu pour passer de l’un à l’autre.
En Flutter, il existe deux types de widget :
- le Stateless widget est utile lorsque la partie de l’interface utilisateur que vous décrivez ne dépend de rien d’autre que des informations de configuration d’objet et du BuildContext dans lequel le widget est initialisé.
- Le Stateful widget: contrairement au Stateless widget, le Stateful widget contient l’instance de l’état du composant. Le Stateful widget se compose de deux parties : l’immutable Statefull widget est l’instance de son état.
[home_page.dart]
Dans notre cas, ce sont les classes HomePage et HomePageState. Le lien entre le widget et son état se fait dans la méthode createState() qui initialise l’instance du state. La plupart du temps, c’est tout ce que doit contenir le Stateful widget. J’ai toutefois une préférence : à titre personnel, j’apprécie d’avoir le code de la vue et son codebehind dans deux objets différents. Pour rendre cela possible, j’ai créé une méthode buildWidget qui se charge du rendu visuel du widget et qui sera appelée lors de la construction de widget.
Le widget contient les trois éléments : un texte statique, un texte qui représente le compteur et le FloatingActionButton pour augmenter le compteur. Vous pouvez constater que le TextWidget du counter est enveloppé par StreamBuilder qui consomme le Stream quand il y a un changement des données.
Pour récupérer le compteur, il suffit de faire snapShot.data.toString() et le TextWidget affichera la valeur du compteur envoyée par HomeBloc. L’event handler de FloatingActionButton se trouve directement dans l’objet state.
Le MyHomePageState représente le code behind du widget. Il contient l’instance du BLoC, les évènements du cycle de vie et le handler des éventements du widget.
[home_page_state.dart]
L’instance du BLoC se récupère dans la méthode didChangeDependencies qui n’est appelée qu’une seule fois, lorsque widget est créé dans la mémoire. Toutes les autres méthodes peuvent être appelées plusieurs fois dans la vie du widget.
La method build est appelée à chaque fois que le framework décide de redessiner le widget.
Et bien sûr, une méthode dispose du BLoC pour libérer l’instance.
Et voilà ! L’application d’exemple « counter » a été modifiée pour utiliser BLoC. Vous savez désormais comment utiliser ce pattern dans vos projets.
Ce tutoriel vous a plus ? Retrouvez les extraits de code sur mon repo GitHub.
N’hésitez pas à partager vos patterns de gestion de l’état préférés dans les commentaires !
Bonjour,
Votre tutoriel est fort intéressant, néanmoins j’ai du louper quelque chose car je n’arrive pas à trouver cette dépendance “Increase_event.dart”
import ‘package:projects/Increase_event.dart’;
Pourriez-vous SVP m’indiquer le nom du package à ajouter dans le pubspec.yaml.
Je vous remercie d’avance
Bonjour,
Merci pour votre commentaire.
L’IncreaseEvent ce n’est pas un package, donc vous n’avez pas besoin de modifier votre pubspec.yaml. C’est une simple class. Vous pouvez le voir dans mon repo GitHub (https://github.com/statk/flutter-ci-cd-azure-devops/blob/master/lib/Increase_event.dart)
Bon courage à vous et n’hésitez pas si vous avez des questions.
[🖊️Cette réponse a été écrite par le rédacteur Kirill]