A la (re)découverte de BLOC Flutter

le 26/03/2024 par Ziyu Ye
Tags: Software Engineering, Mobile

Introduction

Pour un utilisateur, une application est souvent une succession d'écrans avec des listes et des boutons lui permettant de consulter et modifier des informations. Une application est donc capable de gérer à la fois l'interface graphique et les données métiers. La liaison entre ces deux éléments est appelée un State. Chaque modification de données métiers entraîne la mise à jour des States qui lui contient toutes les données nécessaires à l’affichage d’une interface graphique. Deux types de state existent: ephemeral state et application state (app state). Un ephemeral state est local à un endroit précis dans l’application et l’app state lui peut être partagé entre plusieurs écrans de l'application. Pour gérer cet app state, l’utilisation d’une librairie de state management est recommandée. Une librairie de state management a pour objectif de faciliter la gestion de l'évolution des états de l'application tout au long de son exécution.

Dans le contexte ultra-concurrentiel des librairies de State Management en Flutter, il en est une, BLOC, présente quasiment depuis le début, et qui reste toujours d'actualité. Si vous ne la connaissez pas encore, ou que vous vous posez la question de quelle librairie utiliser, vous êtes au bon endroit ! Après avoir rappelé le concept de State Management, on vous dit tout de sa mise en place étape par étape avec Bloc (Business Logic Components).

Bloc

Bloc est une librairie de state management spécifiquement conçue pour Flutter. Présent sur GitHub depuis 2018, ce projet offre une solution robuste pour séparer efficacement la couche de présentation de la couche métier dans les applications Flutter. En adoptant cette approche, le code devient plus modulaire, ce qui favorise sa réutilisabilité, sa maintenabilité et sa testabilité. Cette librairie simplifie la gestion de l'état de l'application, offrant ainsi une expérience de développement fluide et efficace pour les développeurs Flutter. Contrairement à d'autres solutions de state management, Bloc adopte une approche où chaque écran possède ses propres petits states. La philosophie de cette librairie est de favoriser la création de blocs de petite taille propres à chaque écran. Cette approche modulaire permet une meilleure organisation et une plus grande clarté dans la gestion de l'état de l'application, offrant ainsi une solution plus flexible et adaptée à la complexité croissante des applications Flutter.

La théorie sur l’utilisation de BLOC

Dans l’architecture BLOC, chaque Bloc est associé à ses propres events et à son propre state qui sont expliqués ci-dessous. Chaque bloc contient ses logiques métiers, permettant ainsi de mettre à jour le state afin de modifier l’écran.

Lorsqu'un utilisateur clique sur un bouton, cela déclenche un event. Le bloc récupère et traite cet événement en effectuant un appel au repository ou à la base de données, lui permettant de récupérer les données nécessaires pour mettre à jour l'état. Cette mise à jour de l'état entraîne alors une modification de l'écran.

  • L’event : les events sont déclenchés par une action de l'utilisateur et sont ensuite reçus et traités par le Bloc. Souvent, ce sont des objets assez simples.

  • Le bloc : un bloc est initialisé au moment de la création de l'écran. Celui-ci contient la logique métier permettant de traiter l'événement déclenché par l'utilisateur, puis il met à jour le state après ce traitement.

  • Le state : le state représente l’état actuel d’un écran. Il contient toutes les données nécessaires dont on a besoin pour afficher les informations à l'utilisateur. De plus, la mise à jour du state est généralement responsable de déclencher des changements au niveau de la partie graphique.

  • Le repository: le repository est une couche intermédiaire entre l'application et la source de données (que ce soit une base de données, une API externe, un fichier local, etc.).

Exemple de scénario

Dans le cas où un utilisateur d'une application bancaire ayant un solde de 0€ effectue un dépôt de 10€.

  1. État initial : Notre bloc est initialisé avec un repository et prêt à recevoir des event afin de pouvoir les traiter. Notre state contient initialement un solde de 0€.
  2. Notre utilisateur clique sur le bouton “Déposer 10€”, ce qui déclenche la création d’un event ‘Déposer 10€’ qui sera traiter par un bloc.
  3. Le bloc va gérer cet événement. Il fera un appel au repository associé, qui enregistrera dans un back-end ce dépôt. Au retour du repository, notre bloc va emit un state contenant la donnée ‘10€’.
  4. La mise à jour du state va modifier l’interface graphique de notre écran, qui affichera le nouveau solde de ‘10€’.

L’implémentation en Flutter avec flutter_bloc

Dans cette partie, une implémentation du scénario ci-dessus sera présentée.

L’écran

La création de l'écran implique plusieurs étapes essentielles :

  1. Initialisation de l'écran : créer l'interface graphique avec les éléments nécessaires, tels que le texte affichant la somme actuelle et le bouton pour déposer 10€.
  2. Initialisation du Bloc avec le repository : à la création de l'écran, initialiser le Bloc en lui fournissant le repository correspondant. Le repository gérera l'accès aux données, tandis que le Bloc orchestrera la logique métier.
  3. Affichage de la somme actuelle : afficher un texte avec la somme actuelle. Ce texte sera mis à jour grâce au state géré par le Bloc.
  4. Bouton déclenchant une action de dépôt : ajouter un bouton sur l'écran qui, lorsqu'il est cliqué, déclenche une action. Cette action sera traitée par le Bloc, qui appellera le repository pour effectuer le dépôt de 10€.

En résumé, cette implémentation crée une interface utilisateur avec un texte affichant la somme actuelle et un bouton pour déclencher le dépôt de 10€. Le Bloc associé gère la logique métier, tandis que le repository assure la gestion des données.

class DepotSommeScreen extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return BlocProvider(
     create: (BuildContext context) => DepotSommeBloc(
       repository: DepotSommeRepository(),
     ),
     child: BlocBuilder<DepotSommeBloc, DepotSommeState>(
       builder: (context, state) {
         return Column(
           children: [
             Text('Somme actuelle ${state.somme}'),
             ElevatedButton(
               child: Text('Déposer une somme de 10€'),
               onPressed: () {
                 context.read<DepotSommeBloc>().add(DepotSommeEvent(10));
               },
             ),
           ],
         );
       },
     ),
   );
 }
}

BlocProvider permet d’initialiser le Bloc avec le repository correspondant et de créer l’écran en question. BlocBuilder permet de réagir au changement de state, il contient le state actuel de notre bloc afin de mettre à jour l’écran. La librairie Bloc contient d'autres fonctions que BlocBuilder tels que BlocConsumer, BlocListener et BlocSelector, etc qui permet de réagir ou construire le widget lors de la mise à jour du state.

L’Event

L’évent qui sera déclenché par une action utilisateur afin de déposer la somme renseignée. Chaque event possède un sens métier, correspondant à une fonctionnalité particulière au sein de l'application.

class DepotSommeEvent {
 final double somme;

 const DepotSommeEvent(this.somme);
}

Le Bloc

Le bloc reçoit l’event DepotSommeEvent, qui déclenche l’appel au repository afin de déposer la somme renseignée dans l’event et met à jour le state avec la nouvelle somme retourner par le repository. Dans la majorité des cas, le rôle d’un Bloc est de transformer des events en state afin d’afficher des données à l’écran aux utilisateurs.

class DepotSommeBloc extends Bloc<DepotSommeEvent, DepotSommeState> {
 final DepotSommeRepository repository;

 DepotSommeBloc({required this.repository}) : super(DepotSommeState.initial()) {
   on<DepotSommeEvent>(_handleDepotSommeEvent);
 }

 Future<void> _handleDepotSommeEvent(
   DepotSommeEvent event,
   Emitter<DepotSommeState> emit,
 ) async {
   final nouvelleSomme = await repository.depotSomme(event.somme);
   emit(DepotSommeState(somme: nouvelleSomme));
 }
}

A noter qu’un Bloc peut réagir à plusieurs events différents. Il suffit de créer un event parent.

abstract class CompteEvent {}

class DepotSommeEvent extends CompteEvent {
 final double somme;

 const DepotSommeEvent(this.somme);
}

class RetirerSommeEvent extends CompteEvent {
 final double somme;

 const RetirerSommeEvent(this.somme);
}
class CompteBloc extends Bloc<CompteEvent, CompteState> {
 final CompteRepository repository;

 CompteBloc({required this.repository}) : super(CompteState.initial()) {
   on<DepotSommeEvent>(_handleDepotSommeEvent);
   on<RetirerSommeEvent>(_handleRetirerSommeEvent);
 }

 Future<void> _handleDepotSommeEvent(
   DepotSommeEvent event,
   Emitter<CompteState> emit,
 ) async {
   …
 }

 Future<void> _handleRetirerSommeEvent(
   RetirerSommeEvent event,
   Emitter<CompteState> emit,
 ) async {
   …
 }
}

Le state

Le state contient la somme affichée sur l’interface graphique. Il est important de bien concevoir notre state. Chaque state doit être la source de vérité, ne pas dupliquer de données dans d’autres states sinon la gestion de données va devenir plus compliquée.

class DepotSommeState extends Equatable {
 final double somme;

 DepotSommeState({required this.somme});

 factory DepotSommeState.initial() {
   return DepotSommeState(somme: 0);
 }

 @override
 List<Object?> get props => [somme];
}

Le Repository

Le repository a pour rôle d'interagir avec une API externe pour effectuer un dépôt de somme, suivi de la réception de la nouvelle somme associée au compte.

class DepotSommeRepository {
 Future<double> depotSomme(double somme) async {
   // fait un appel au API afin de déposer la somme
   const nouvelleSomme = 10.0;
   return nouvelleSomme; // retourne la nouvelle somme
 }
}

Conclusion

Comme vous pouvez le constater, l'utilisation de Bloc facilite la gestion de nos states. La maintenabilité et la testabilité sont grandement optimisées grâce à la séparation entre l'interface utilisateur (UI) et la logique métier.

Même si l'architecture de Bloc semble simple et souple, elle demande une certaine standardisation dès le début du projet avec l’ensemble de l'équipe de développement, par exemple l’utilisation de BlocSelector transformant notre state en un viewModel qui contient les données bien formatées et prêt à être afficher directement à l’écran. Cela permet de mettre en place une cohérence dans l'ensemble du code. Cependant, il est important de noter que la philosophie fondamentale de la librairie repose sur des blocs de petite taille, spécifiquement conçus pour être utilisés dans un écran donné. La mise en place d'une solution appropriée de partage de données entre écrans demande un peu plus d'efforts, mais cela peut être bien maîtrisé avec une compréhension approfondie de la librairie Bloc. (exemples: https://bloclibrary.dev/#/architecture?id=bloc-to-bloc-communication)

Bloc est idéal pour vous si:

  • Vous recherchez une progression rapide dès le premier jour.
  • Vous souhaitez une plus grande liberté dans la conception de l'architecture de votre application.
  • Vous préférez une syntaxe légère et facile à écrire.

Cependant, Bloc peut ne pas convenir si :

  • Vous avez un grand volume de données à partager entre les écrans de votre application.
  • Votre équipe n'est pas encore mature sur la techno et a besoin d'un cadre clair et bien défini.