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.
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.).
Dans le cas où un utilisateur d'une application bancaire ayant un solde de 0€ effectue un dépôt de 10€.
Dans cette partie, une implémentation du scénario ci-dessus sera présentée.
La création de l'écran implique plusieurs étapes essentielles :
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’é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 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 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 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
}
}
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:
Cependant, Bloc peut ne pas convenir si :