← Retour au blog

1 mai 2026 · 5 min de lecture

Flutter BloC en production : ce que j'ai appris en 3 ans

Retour d'expérience sur l'utilisation de Cubit et BloC dans des applications Flutter maintenues, avec les compromis qui comptent vraiment.

J’ai utilisé Flutter sur plusieurs applications avec des contraintes différentes : refonte complète, produit métier, expérimentation mobile, application sociale. Dans ces contextes, BloC n’a pas été intéressant parce qu’il est populaire, mais parce qu’il impose une discipline utile quand l’application grossit.

Le pattern n’empêche pas d’écrire du mauvais code. Il rend surtout plus visibles les endroits où la logique commence à déborder : appels réseau dans l’UI, états ambigus, écrans qui reconstruisent trop souvent, dépendances entre features mal isolées.

Cubit par défaut, BloC quand il apporte quelque chose

Je commence presque toujours avec Cubit. C’est direct : une classe, des méthodes, des états. Pour un écran de profil, un formulaire simple ou un chargement de liste, c’est largement suffisant.

class ProfileCubit extends Cubit<ProfileState> {
  ProfileCubit(this._repository) : super(const ProfileState.initial());

  final ProfileRepository _repository;

  Future<void> load(String userId) async {
    emit(const ProfileState.loading());

    final result = await _repository.getProfile(userId);
    emit(result.fold(ProfileState.error, ProfileState.loaded));
  }
}

Je passe à BloC quand les événements eux-mêmes deviennent importants : recherche avec debounce, pagination, annulation d’une requête, synchronisation avec un stream, ou enchaînement d’actions utilisateur.

En pratique, la majorité des features restent en Cubit. BloC doit résoudre une complexité réelle, pas servir de standard par défaut.

Les repositories ne sont pas optionnels

L’erreur que j’ai le plus vue consiste à mettre la logique métier directement dans le Cubit : requête HTTP, mapping JSON, fallback cache, analytics, puis émission d’état. Au début, ça paraît rapide. À la troisième évolution, le Cubit devient impossible à tester proprement.

Le Cubit doit orchestrer l’état. Le repository doit gérer la donnée.

// À éviter : le Cubit connaît trop de détails.
Future<void> loadFeed() async {
  final response = await http.get(Uri.parse('/api/feed'));
  final data = json.decode(response.body) as List;
  final events = data.map((e) => Event.fromJson(e)).toList();

  emit(FeedState.loaded(events));
}

// Plus durable : le Cubit pilote, le repository travaille.
Future<void> loadFeed() async {
  emit(const FeedState.loading());

  final result = await _feedRepository.getFeed();
  emit(result.fold(FeedState.error, FeedState.loaded));
}

Cette séparation aide aussi côté produit. Quand une règle change, on sait où intervenir. Quand un écran bug, on peut tester l’état sans démarrer toute l’application.

Des états explicites plutôt que des booléens

Un écran mobile finit souvent avec trop de booléens : isLoading, hasError, isRefreshing, isEmpty, isSubmitting. Le problème n’est pas seulement esthétique. Certaines combinaisons n’ont aucun sens, mais le code les autorise.

Je préfère des états exhaustifs :

sealed class FeedState {
  const FeedState();
}

class FeedInitial extends FeedState {}
class FeedLoading extends FeedState {}
class FeedLoaded extends FeedState {
  const FeedLoaded(this.items);
  final List<Event> items;
}
class FeedEmpty extends FeedState {}
class FeedFailure extends FeedState {
  const FeedFailure(this.message);
  final String message;
}

Avec freezed, cette approche devient encore plus agréable. Le point important est de modéliser l’expérience utilisateur, pas seulement la donnée. Un écran vide, une erreur réseau et un refresh en cours ne racontent pas la même chose.

Les dépendances entre Cubits doivent rester rares

Un cas fréquent : une feature doit réagir à l’authentification. Par exemple, réinitialiser le chat quand l’utilisateur se déconnecte. Injecter directement AuthCubit dans ChatCubit fonctionne, mais crée vite un couplage désagréable.

Je préfère passer un stream ou un service d’orchestration, selon le besoin.

class ChatCubit extends Cubit<ChatState> {
  ChatCubit({
    required ChatRepository repository,
    required Stream<AuthState> authStateStream,
  }) : _repository = repository,
       super(const ChatState.initial()) {
    _authSubscription = authStateStream.listen((state) {
      if (state is AuthUnauthenticated) {
        reset();
      }
    });
  }

  final ChatRepository _repository;
  late final StreamSubscription<AuthState> _authSubscription;

  void reset() {
    emit(const ChatState.initial());
  }

  @override
  Future<void> close() {
    _authSubscription.cancel();
    return super.close();
  }
}

Le test devient simple : un StreamController<AuthState> suffit. Le Cubit ne dépend pas de l’implémentation complète de l’authentification.

Construire moins souvent, écouter au bon endroit

La différence entre BlocBuilder, BlocListener et BlocConsumer paraît scolaire, mais elle a un vrai impact en production.

BlocBuilder sert à reconstruire l’interface. BlocListener sert aux effets de bord : navigation, snackbar, tracking, fermeture d’une modale. BlocConsumer est pratique, mais je l’utilise avec parcimonie. Quand tout est mélangé, on finit par déclencher une navigation au milieu d’une logique d’affichage.

BlocBuilder<ProfileCubit, ProfileState>(
  buildWhen: (previous, current) =>
      previous.runtimeType != current.runtimeType,
  builder: (context, state) {
    return switch (state) {
      ProfileLoading() => const CircularProgressIndicator(),
      ProfileLoaded(:final profile) => ProfileView(profile: profile),
      ProfileFailure(:final message) => ErrorView(message: message),
      _ => const SizedBox.shrink(),
    };
  },
)

Le buildWhen n’est pas une optimisation prématurée quand l’écran contient des listes, des images ou des composants coûteux. C’est une manière de dire explicitement quels changements concernent vraiment l’UI.

Ce que j’applique aujourd’hui

Sur une nouvelle application Flutter, je garde quelques règles simples :

  • Cubit pour les cas standards, BloC seulement quand le flux d’événements le justifie ;
  • repositories systématiques pour sortir la donnée des Cubits ;
  • états explicites plutôt qu’une accumulation de booléens ;
  • dépendances entre features via streams ou services, pas par injection sauvage de Cubits ;
  • tests unitaires sur les Cubits importants, surtout ceux qui gèrent des formulaires ou de la synchronisation.

BloC n’est pas magique. Il ne remplace pas une bonne découpe produit, ni une vraie réflexion sur les états d’écran. Mais sur des applications maintenues dans le temps, il donne un cadre solide : le comportement devient plus prévisible, les régressions sont plus faciles à isoler, et l’équipe parle le même langage quand elle fait évoluer une feature.