Séparer BLoC et cas d'usage en Flutter : Architecture propre en action
Le pattern BLoC de Flutter se transforme souvent en objet Dieu : les gestionnaires d'événements mélangent appels de services, validation et émission d'états. Cela rend la maintenance, les tests et la mise à l'échelle cauchemardesques. Déplacer la logique métier vers des cas d'usage restaure une séparation correcte des couches selon les principes d'architecture propre : présentation (UI et BLoC), domaine (cas d'usage, entités) et données (dépôts, services).
Un cas d'usage implémente un scénario spécifique : il orchestre les services, gère les erreurs, applique les règles métier et reste indépendant de l'UI. Le BLoC se concentre uniquement sur le flux de données unidirectionnel — gestion des événements et émission d'états.
BLoC classique : Où la logique s'enchevêtre avec l'UI
Un BLoC typique appelle directement les services via injection de dépendances, en effectuant des effets secondaires directement dans les gestionnaires. Voici un exemple simplifié de ItemsBloc :
class ItemsBloc extends Bloc<ItemsEvent, ItemsState> {
final FetchItemsService _service;
ItemsBloc(this._service) : super(Initial()) {
on<FetchItemsEvent>(_onFetchItemsEvent);
}
Future<void> _onFetchItemsEvent(FetchItemsEvent event, Emitter<ItemsState> emit) async {
emit(Loading());
try {
final items = await _service.fetchItems();
emit(Loaded(items: items));
} catch (error) {
emit(Error(error: error));
}
}
}
Problèmes : couplage fort, code difficile à tester et violations du principe de responsabilité unique (SRP). Le BLoC en sait trop sur les données, le réseau et la logique métier.
Refactoring : BLoC comme gestionnaire d'état pur
Après refactoring, le BLoC ne fait que mapper les événements aux états, sans dépendances :
class ItemsBloc extends Bloc<ItemsEvent, ItemsState> {
ItemsBloc() : super(Initial()) {
on<SetLoading>(_onSetLoading);
on<SetLoaded>(_onSetLoaded);
on<SetError>(_onSetError);
}
Future<void> _onSetLoading(SetLoading event, Emitter<ItemsState> emit) async {
emit(Loading());
}
Future<void> _onSetLoaded(SetLoaded event, Emitter<ItemsState> emit) async {
emit(Loaded(items: event.items));
}
Future<void> _onSetError(SetError event, Emitter<ItemsState> emit) async {
emit(Error(error: event.error));
}
}
Le cas d'usage gère l'orchestration :
class FetchItemsUseCase {
final ItemsBloc bloc;
final FetchItemsService service;
FetchItemsUseCase({required this.bloc, required this.service});
Future<void> call() async {
bloc.add(SetLoading());
try {
final items = await service.fetchItems();
bloc.add(SetLoaded(items: items));
} catch (error) {
bloc.add(SetError(error: error.toString()));
}
}
}
Le BLoC se réduit à 20 lignes et devient totalement testable : simuler des événements et vérifier les états.
Avantages des cas d'usage comme orchestrateurs
Un cas d'usage n'est pas qu'un proxy — il combine des données de dépôts, applique des règles, journalise l'activité et gère la synchronisation. Il dépend d'abstractions (interfaces), pas d'implémentations concrètes.
Avantages clés :
- Testabilité : Simuler services/dépôts pour tester les cas d'usage en unité.
- Évolutivité : Ajouter de nouveaux scénarios sans toucher au BLoC (principe Ouvert/Fermé).
- Flexibilité DI : Injecter des services dans les cas d'usage ; le BLoC reste indépendant.
- Lisibilité : Les scénarios métier sont isolés dans des classes dédiées.
Dans un projet réel, ce refactoring a réduit le temps de développement de 30 %, diminué les bugs et simplifié les tests.
Flux de données et compromis
UI → cas d'usage → services/dépôts → événements vers BLoC → état UI. Le cas d'usage connaît le BLoC (compromis d'architecture propre pour la simplicité), mais l'UI est libérée des appels bloc.add.
Le BLoC assure un flux de données unidirectionnel : comportement prévisible et tests faciles. Les cas d'usage sont testés indépendamment : simuler des réponses et vérifier les appels.
Points clés à retenir
- Séparation des couches : BLoC comme gestionnaire d'état, cas d'usage pour la logique domaine.
- SRP en action : Chaque classe a une seule responsabilité.
- Tests simplifiés : Fonctions pures dans le BLoC, mocks dans les cas d'usage.
- Évolutivité : Compatible avec Cubit, Riverpod ou autres gestionnaires d'état.
Cette approche convient à tout projet Flutter où le BLoC gonfle. Mesurez les métriques avant/après : tailles de classes, couverture de tests, temps de livraison de fonctionnalités.
— Editorial Team
Aucun commentaire pour le moment.