Comme d’habitude, voici mes recommandations en bref pour ceux qui n’ont pas le temps de toute lire 🚀

Si tu débutes, avec une petite app, commence avec Provider:
  • Simple à comprendre
  • Recommandé officiellement par l'équipe Flutter
  • Bien documenté avec des tonnes de tutos partout
  • Une transition naturelle depuis ```setState()```
👉🏻 C'est top pour apprendre les concepts fondamentaux du state management (séparer l'état de l'UI, partager des données, écouter des changements), sans se noyer dans la complexité.
Si tu souhaites scaler ton app, choisis Riverpod si:
  • Tu développes seul ou en petite équipe
  • Tu veux de la flexibilité dans l'architecture
  • La compile-time safety et AsyncValue te font de l'oeil
  • Tu veux quelque chose de moderne et bien pensé
👉🏻 Riverpod, c'est l'évolution naturelle de Provider. D'ailleurs, si tu maîtrises Provider, tu comprends déjà 80% de Riverpod. C'est plus puissant, plus safe, mais toujours flexible. Bref, un très bon choix.
Si tu souhaites scaler ton app, choisis Bloc Si:
  • Tu travaille en équipe (5+ devs)
  • Tu veux une architecture stricte et uniforme
  • Le testing rigoureux et le time-travel debugging sont importants pour toi
  • Tu valorises la séparation UI/logique absolue
👉🏻 Bloc, c'est le choix entreprise. Plus verbeux, plus rigide mais aussi plus structuré. Si ton app est vouée à devenir grosse et complexe, Bloc c'est un excellent cadre pour ne pas partir dans tous les sens.

Bon ça doit être une des première fois que je ne suis pas vraiment mes propres conseils 🙃 Si ça t’intéresse, tu peux aller directement lire la partie ce que j’utilise concrètement, ou plus généralement te rendre à la section conclusion pour un peu plus de détails.

Et si tu as du temps, je t’invite évidemment à tout lire, et à me donner ton avis. Bonne lecture !

Introduction

Si tu développes avec Flutter, il y a un moment où tu vas te retrouver face à la question qui fait débat dans la communauté : comment gérer l’état de mon app ? Et de ce que j’en vois, j’ai l’impression que c’est un sujet qui fait couler presque autant d’encre que de larmes.

C'est quoi, la "gestion d'état" ?

Alors, commençons par les bases. La gestion d’état, c’est tout simplement la façon dont ton app gère et partage les données qui peuvent changer au fil du temps.

Imagine ton app comme une maison. L’état, ce sont toutes les choses qui peuvent varier : la température, les lumières allumées ou éteintes, qui est à la maison, ce qu’il y a dans le frigo. Et la gestion d’état, c’est le système qui permet à toutes les pièces de la maison de savoir ce qui se passe ailleurs.

Concrètement, dans une app, l’état, c’est :

  • L'utilisateur est-il connecté ? (et qui est-il ?)
  • Le mode sombre est-il activé ?
  • Quelle page l'utilisateur consulte-t-il en ce moment ?
  • Les données sont-elles en train de charger ?

Le problème, c’est que ces données doivent souvent être accessibles depuis plusieurs endroits de ton app. Et si l’utilisateur se déconnecte, tu veux que toute l’interface réagisse : retour à l’écran de login, suppression des données en cache, arrêt des requêtes en cours, etc.

Et c’est là que ça se complique. Comment partager l’info du profil utilisateur entre 10 widgets différents sans passer des paramètres dans tous les sens ?

C’est exactement ça, la gestion d’état. Et bien le faire, c’est ce qui différencie une app propre et maintenable d’un gros plat de spaghettis impossible à débugger.

Pourquoi c'est un casse-tête pour tous les devs Flutter

Le truc avec Flutter, c’est qu’il te donne beaucoup (trop) de choix. Contrairement à React qui a largement standardisé autour de hooks et Context API, ou à SwiftUI qui pousse Combine, Flutter te laisse complètement libre.

Et cette liberté, elle est à la fois géniale et paralysante.

Quand tu démarres avec Flutter, tu utilises setState() parce que c’est ce que montrent tous les tutos. Ça marche super bien… jusqu’à ce que ton app grandisse et que tu te retrouves avec des setState() partout, des widgets qui se rebuild 50 fois par seconde, et un code impossible à suivre.

Alors tu te dis “OK, il me faut une vraie solution de state management”. Tu googles, et là, c’est le choc :

  • Provider (recommandé par Google)
  • Riverpod (la nouvelle version de Provider)
  • Bloc (très populaire en entreprise)
  • GetX (adoré par certains, détesté par d'autres)
  • MobX, Redux, InheritedWidget, et j'en passe...

Chacun a ses fans, ses détracteurs, sa philosophie, sa courbe d’apprentissage. Et sur Reddit ou Stack Overflow, tu tombes sur des débats enflammés où chacun défend sa solution comme si sa vie en dépendait.

Résultat ? Tu passes plus de temps à comparer des solutions qu’à coder ton app. Tu procrastines en regardant des tutos “Provider vs Bloc” pendant que ton projet n’avance pas. Je sais, je suis passée par là.

Le pire, c’est qu’il n’y a pas de mauvais choix. Toutes ces solutions fonctionnent. Le vrai enjeu, c’est de choisir celle qui correspond à ton niveau, ton projet, et ta façon de penser.

Ce que tu vas apprendre dans cet article

Mon objectif ici n’est pas de faire un énième tuto, mais de te donner plutôt un guide concret pour t’aider à choisir.

On va explorer les principales solutions (Provider, Riverpod, Bloc, GetX), avec pour chacune :

  • Comment ça marche vraiment
  • Les forces et les faiblesses honnêtes
  • Quand l'utiliser (et quand l'éviter)

Et à la fin, je te donnerai mon verdict selon le type de projet. Parce que, comme presque toujours j’ai l’impression: il n’y a pas une solution miracle qui marche pour tout. Un side project de weekend n’a pas les mêmes besoins qu’une app d’entreprise avec 10 devs.

L’idée, c’est qu’à la fin de cet article, tu puisses faire un choix éclairé et arrêter de procrastiner. Parce que le meilleur state management, c’est celui qui te permet de shipper ton app, pas celui qui a le plus d’étoiles sur GitHub.

setState : la base (et ses limites)

Commençons par le commencement : setState(). C’est la première chose que tu apprends quand tu débutes avec Flutter, et c’est aussi la solution de gestion d’état la plus simple qui existe.

Comment ça marche

setState(), c’est la méthode built-in de Flutter pour dire “Hey, mes données ont changé, rebuild mon widget !”. C’est aussi simple que ça.

Prenons un exemple classique : un compteur.

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Compteur: $_counter'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ),
    );
  }
}

Quand tu appuies sur le bouton, setState() est appelé, Flutter reconstruit le widget, et le nouveau compteur s’affiche.

Le truc important à comprendre : setState() dit à Flutter de relancer la méthode build() de ton widget. Tout ce qui est dans build() est recalculé et redessiné. C’est pour ça que tu dois mettre tes données qui changent (comme _counter) dans le State, pas dans le widget lui-même.

Quand c'est suffisant

setState() n’est pas une solution bas de gamme ou “pour débutants”. Il y a plein de cas où c’est exactement ce qu’il te faut. Voyons quelques exemples.

Pour de l'état local à un seul widget

Une checkbox cochée ou non, un TextField qui change, un onglet sélectionné dans un TabBar,… Tout ça, c’est parfait pour setState().

class TodoItem extends StatefulWidget {
  final String title;
  
  TodoItem({required this.title});
  
  @override
  _TodoItemState createState() => _TodoItemState();
}

class _TodoItemState extends State<TodoItem> {
  bool _isCompleted = false;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text(widget.title),
      value: _isCompleted,
      onChanged: (value) {
        setState(() {
          _isCompleted = value ?? false;
        });
      },
    );
  }
}

Ici, l’état _isCompleted n’intéresse que ce widget. Pas besoin d’une architecture complexe, setState() fait le job.

Pour des prototypes rapides

Si tu testes une idée, ou que tu veux juste voir si un concept fonctionne, setState() te permet d’aller vite. Pas de setup, pas de boilerplate, tu codes et ça marche.

Pour des apps très simples

Si ton app a 2-3 écrans avec peu d’interactions entre eux, setState() peut très bien suffire. Pas besoin de sortir l’artillerie lourde pour une calculatrice ou un convertisseur d’unités.

Pourquoi ça devient vite le bordel

Bon, maintenant, parlons des problèmes. Parce que oui, setState() a ses limites, et elles arrivent vite.

Le partage d'état entre widgets devient un cauchemar

Imagine que tu as ton compteur, mais maintenant tu veux l’afficher à la fois sur la page principale ET dans un drawer. Comment tu fais ?

Option 1 : Tu passes la valeur en paramètre

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: MyDrawer(counter: _counter),  // Passer en param
      body: CounterDisplay(counter: _counter),  // Passer en param
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _counter++;
          });
        },
      ),
    );
  }
}

Ça marche, mais imagine maintenant que MyDrawer a lui-même 3 niveaux de widgets imbriqués avant d’afficher le compteur. Tu dois passer counter à travers tous ces widgets, même ceux qui n’en ont rien à faire. C’est ce qu’on appelle le prop drilling, et c’est l’enfer.

Option 2 : Tu remontes l’état en haut de l’arbre de widgets.

Mais du coup, à chaque changement, c’est toute ton app qui se rebuild. Bonjour les problèmes de performance.

La logique métier se mélange avec l'UI

Avec setState(), tout ton code business est dans tes widgets. Tu te retrouves avec des State classes qui font 500 lignes, qui gèrent à la fois l’affichage, la logique, les appels API, la validation de formulaires…

Bon courage pour tester ça, ou pour réutiliser cette logique ailleurs.

Les rebuilds inutiles

Chaque fois que tu appelles setState(), tout le widget se rebuild. Même les parties qui n’ont pas changé.

Sur une petite app, c’est pas grave. Sur une grosse app avec des listes longues, des animations, des images… ça peut devenir un vrai problème de performance. Et tu te retrouves à devoir optimiser avec des const partout, découper tes widgets en micro-widgets, utiliser des ValueListenableBuilder… Bref, on passe vite de l’atelier dev à l’atelier brico.

Le testing devient compliqué

Comment tu testes la logique d’ajout de tâches si elle est coincée dans un StatefulWidget ? Tu dois créer tout le widget, simuler des interactions… C’est lourd. Avec une vraie solution de state management, ta logique est séparée et facilement testable.

L'état est perdu au moindre changement de route

Si tu navigues vers une autre page puis tu reviens, ton State est recréé. Ton compteur ? Remis à zéro. Tes filtres de recherche ? Oubliés. Bien sûr, tu peux contourner ça avec des AutomaticKeepAliveClientMixin ou en stockant dans un singleton… mais là, tu es en train de réinventer un state management.

👉🏻 En résumé sur setState() : c’est parfait pour de l’état local simple, pour prototyper vite, ou pour des petites apps. Mais dès que ton app grandit, que tu as besoin de partager de l’état entre plusieurs écrans, ou que ta logique métier devient un peu complexe, setState() montre ses limites. Et c’est là qu’il faut passer à une vraie solution de state management.

Provider : le compromis parfait pour débuter

Si setState() commence à te poser problème, Provider est probablement la première solution vers laquelle tu vas te tourner. Et pour cause : c’est la solution officiellement recommandée par l’équipe Flutter elle-même.

Présentation et philosophie

Provider, c’est un package créé par Remi Rousselet (un dev très actif dans la communauté Flutter) qui facilite le partage d’état dans ton app. L’idée centrale est plutôt simple: mettre ton état “au-dessus” de ton arbre de widgets, et permettre à n’importe quel widget descendant d’y accéder facilement.

La philosophie de Provider repose sur InheritedWidget, un mécanisme built-in de Flutter pour propager des données dans l’arbre de widgets. Mais InheritedWidget, c’est verbeux et pas très user-friendly. Provider l’enveloppe et te donne une API simple et élégante.

Le pattern classique avec Provider :

  • Tu crées une classe qui contient ton état
  • Tu fournis cette classe en haut de ton app
  • N'importe quel widget peut consommer cet état

C’est aussi simple que ça ! Voyons un exemple concret, ça aide toujours à y voir plus clair.

Exemple concret avec ChangeNotifier

Reprenons notre compteur, mais cette fois avec Provider.

Étape 1 : Créer la classe d’état

class CounterModel extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners();  // Dit à Provider de notifier tous les widgets qui écoutent
  }

  void decrement() {
    _counter--;
    notifyListeners();
  }
}

Ici, ChangeNotifier est une classe fournie par Flutter qui implémente le pattern Observer. Quand tu appelles notifyListeners(), tous les widgets qui écoutent ce modèle sont notifiés et se rebuild.

Étape 2 : Fournir le modèle en haut de l’app

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MyApp(),
    ),
  );
}

Maintenant, CounterModel est accessible depuis n’importe où dans ton app.

Étape 3 : Consommer l’état dans tes widgets

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Consumer<CounterModel>(
          builder: (context, counter, child) {
            return Text('Compteur: ${counter.counter}');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<CounterModel>().increment();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

Quelques points importants :

  • Consumer<CounterModel> écoute les changements et rebuild uniquement ce widget quand notifyListeners() est appelé
  • context.read<CounterModel>() accède au modèle sans écouter les changements (parfait pour appeler des méthodes)
  • Tu peux aussi utiliser context.watch<CounterModel>() pour écouter directement dans le build()

Forces et faiblesses

Points forts de Provider

Simplicité : La courbe d’apprentissage est douce. Si tu comprends ChangeNotifier et le concept de notifyListeners(), tu as compris 90% de Provider.

Recommandé officiellement : L’équipe Flutter le pousse, donc il y a une tonne de ressources, de tutos, d’exemples. Si tu bloques, tu trouves toujours de l’aide.

Flexible : Provider ne te force pas dans une architecture particulière. Tu organises ton code comme tu veux. Ça peut être un avantage… ou un piège (on y revient).

Performance : Grâce à Consumer et Selector, tu peux optimiser précisément quels widgets rebuild quand l’état change. Pas de rebuild inutiles si tu fais attention.

Plusieurs providers : Tu peux avoir plusieurs modèles différents (UserModel, TodoModel, SettingsModel…) et les combiner facilement avec MultiProvider.

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => UserModel()),
    ChangeNotifierProvider(create: (_) => TodoModel()),
    ChangeNotifierProvider(create: (_) => SettingsModel()),
  ],
  child: MyApp(),
)

Facile à tester : Ta logique est dans des classes séparées, pas dans des widgets. Tu peux les tester unitairement sans créer de widgets.

Points faibles de Provider

Trop de liberté : Provider ne t’impose rien. Tu peux organiser ton code comme tu veux… ce qui veut dire que tu peux aussi faire n’importe quoi. Sur un gros projet avec plusieurs devs, ça peut vite partir en cacahuètes si tout le monde a sa propre approche.

ChangeNotifier, c’est fragile : Si tu oublies d’appeler notifyListeners(), ton UI ne se met pas à jour. Si tu l’appelles trop souvent, tu as des problèmes de performance. Il faut faire attention.

Dépendance au context : Tu as besoin du BuildContext pour accéder à tes providers . Ça peut être pénible dans certains cas (appeler une méthode depuis un callback asynchrone par exemple).

Pas de time-travel debugging : Contrairement à Redux ou Bloc, tu ne peux pas facilement revenir en arrière dans l’historique des états pour debugger.

Quand l'utiliser

Provider est parfait pour :

  • Les projets de taille petite à moyenne : Si tu as une app avec 5-20 écrans, Provider fait très bien le job sans te surcharger de complexité.
  • Quand tu débutes avec le state management : C'est la transition naturelle depuis setState(). Pas de concepts compliqués à apprendre.
  • Quand tu développes seul ou en petite équipe : Provider te donne la flexibilité de coder comme tu le sens. C'est bien quand tu n'as pas besoin d'imposer une architecture stricte.

Quand éviter Provider:

  • Si tu as une grosse app d'entreprise avec plein de devs : Provider peut manquer de structure. Bloc ou une architecture plus stricte pourrait être mieux.
  • Si tu as besoin de debugging avancé : avec time-travel, replay d'events, etc... Provider n'offre pas ça out of the box.

Riverpod : Provider sous stéroïdes

Si tu as compris Provider, tu vas adorer (ou détester, selon ton mood) Riverpod. C’est littéralement Provider réécrit par le même dev (Remi Rousselet) pour corriger tous les défauts qu’il a identifiés avec le temps.

D’ailleurs, fun fact: Riverpod = anagramme de Provider ! J’adore ce genre d’info, et j’aime beaucoup l’idée 🙂. Bref, allons-y.

Les différences avec Provider

Pas de dépendance au BuildContext

On en a parlé dans les points faibles de Provider, tu dois toujours avoir un BuildContext pour accéder à ton état. C’est pénible dans certaines situations, et du coup Riverpod règle ça. Tu peux accéder à tes providers de n’importe où, sans context.

// Provider - besoin du context
context.read<CounterModel>().increment();

// Riverpod - pas besoin de context
ref.read(counterProvider.notifier).increment();

Compile-time safety

Avec Provider, si tu fais une typo ou que tu essaies d’accéder à un provider qui n’existe pas, tu ne le sais qu’au runtime, aka quand l’app crash.

Avec Riverpod, c’est vérifié à la compilation. Si ton provider n’existe pas, ton code ne compile même pas.

Pas besoin de MultiProvider

Avec Provider, tu devais wrapper ton app dans un MultiProvider pour fournir plusieurs modèles. Avec Riverpod, les providers sont globaux par défaut. Tu les déclares, et ils sont disponibles partout.

// Provider
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => CounterModel()),
    ChangeNotifierProvider(create: (_) => TodoModel()),
  ],
  child: MyApp(),
)

// Riverpod
void main() {
  runApp(
    ProviderScope(  // Un seul wrapper pour tout
      child: MyApp(),
    ),
  );
}

Testabilité améliorée

Avec Riverpod, tu peux facilement override un provider pendant les tests. Genre remplacer ton vrai service API par un mock. C’est super propre.

Immutabilité encouragée

Provider pousse vers du mutable avec ChangeNotifier. Riverpod encourage l’immutabilité avec ses StateNotifier et autres. C’est plus safe, moins de bugs bizarres.

Les types de providers (StateProvider, FutureProvider, StreamProvider...)

Riverpod ne se contente pas de remplacer ChangeNotifier. Il propose plein de types de providers pour différents cas d’usage.

Provider (le basique)

Pour des valeurs qui ne changent jamais ou presque.

final apiUrlProvider = Provider<String>((ref) {
  return 'https://api.monapp.com';
});

// Utilisation
final url = ref.read(apiUrlProvider);

StateProvider

Pour de l’état simple (un booléen, un int, une string).

final counterProvider = StateProvider<int>((ref) => 0);

// Lecture
final count = ref.watch(counterProvider);

// Modification
ref.read(counterProvider.notifier).state++;

C’est l’équivalent de setState() mais avec les avantages de Riverpod.

StateNotifierProvider

Pour de l’état plus complexe avec de la logique. C’est le remplaçant de ChangeNotifier.

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);  // État initial = 0

  void increment() {
    state++;  // Modifie l'état et notifie automatiquement
  }

  void decrement() {
    state--;
  }
}

final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

Dans ton widget :

class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Scaffold(
      body: Center(child: Text('Compteur: $count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).increment();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

Note le ConsumerWidget au lieu de StatelessWidget. C’est le widget de base de Riverpod qui te donne accès au ref.

FutureProvider

Pour des données asynchrones (appels API, lecture de fichiers…).

final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get('https://api.monapp.com/user');
  return User.fromJson(jsonDecode(response.body));
});

// Dans ton widget
class UserPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);
    
    return userAsync.when(
      data: (user) => Text('Hello ${user.name}'),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Erreur: $error'),
    );
  }
}

La méthode .when() gère automatiquement les 3 états : loading, data, error. C’est super pratique.

StreamProvider

Pour des streams (Firebase Firestore, websockets…).

final tasksStreamProvider = StreamProvider<List<Task>>((ref) {
  return FirebaseFirestore.instance
      .collection('tasks')
      .snapshots()
      .map((snapshot) => snapshot.docs.map((doc) => Task.fromFirestore(doc)).toList());
});

Pareil, dans ton widget, tu utilises .when() et ça gère tout.

Forces et faiblesses

Points forts de Riverpod

Compile-time safety : le gros avantage sur Provider. Tu ne peux pas accéder à un provider qui n’existe pas, tu ne peux pas faire de typo. Ton IDE te guide, et si ça compile, ça marche (enfin, presque toujours, tu connais).

Pas de dépendance au context : c’est un vrai game changer. Tu peux accéder à tes providers depuis n’importe où : dans des méthodes statiques, des callbacks asynchrones, des classes utilitaires… Plus besoin de passer le BuildContext partout.

AsyncValue avec .when() : gérer loading/data/error devient trivial. Tu n’as plus à écrire 50 lignes de if (isLoading) ... else if (hasError) ... else .... Riverpod le fait pour toi.

asyncValue.when(
  data: (data) => ShowData(data),
  loading: () => CircularProgressIndicator(),
  error: (error, stack) => ErrorWidget(error),
)

C’est élégant et ça évite d’oublier de gérer un cas.

Testing simplifié : override un provider en test, c’est juste :

ProviderScope(
  overrides: [
    userProvider.overrideWithValue(mockUser),
  ],
  child: MyWidget(),
)

Tu peux mocker n’importe quoi facilement. Tes tests deviennent beaucoup plus propres.

Pas de boilerplate initial : pas besoin de wrapper ton app dans 10 providers différents. Tu déclares tes providers globalement, et c’est bon. Le setup est minimal.

Auto-dispose : Riverpod dispose automatiquement les providers qui ne sont plus utilisés. Avec Provider, tu devais gérer ça manuellement. Moins de memory leaks.

Points faibles de Riverpod

Courbe d’apprentissage plus raide : Riverpod a plus de concepts à apprendre : StateNotifier, AsyncValue, les différents types de providers (StateProvider vs StateNotifierProvider vs FutureProvider…). Pour quelqu’un qui débute, ça peut être intimidant.

Syntaxe plus verbeuse :

// Provider
context.read<CounterModel>().increment();

// Riverpod
ref.read(counterProvider.notifier).increment();

Ce .notifier à chaque fois, ça alourdit un peu le code. Et il faut penser à utiliser ConsumerWidget ou Consumer pour avoir accès au ref.

Moins de ressources communautaires : Provider existe depuis plus longtemps et a beaucoup plus de tutos, d’exemples, de packages compatibles. Riverpod grandit vite, mais il y a encore moins de contenu disponible. Quand tu bloques sur un truc spécifique, c’est parfois plus dur de trouver de l’aide (quoi que, dans l’air de l’IA, c’est de moins en moins vrai 🙃).

Changements fréquents : Riverpod évolue vite. Entre la version 1.0 et 2.0, il y a eu des breaking changes. C’est bien parce que ça s’améliore, mais ça peut être frustrant de devoir refactorer régulièrement.

Deux syntaxes coexistent : il y a l’ancienne syntaxe (avec StateNotifierProvider) et la nouvelle syntaxe avec code generation (avec @riverpod). C’est mieux, mais ça ajoute de la complexité et de la confusion pour les débutants.

Overkill pour des petits projets : si ton app a 3 écrans et peu d’état partagé, Riverpod apporte peut-être plus de complexité que de valeur. setState() ou Provider classique peuvennt suffir amplement.

Quand l'utiliser

Riverpod est top si:

  • Tu utilises déjà Provider et tu veux upgrader: la migration de Provider vers Riverpod est assez simple. Tes concepts restent les mêmes, juste la syntaxe change. Si tu es à l'aise avec Provider, passer à Riverpod est naturel.
  • Tu veux de la compile-time safety: si tu détestes les bugs runtime bêtes (typos, providers manquants), Riverpod te sauve. Tout est vérifié à la compilation.
  • Tu as besoin d'accéder à l'état sans BuildContext: les callbacks asynchrones, les méthodes utilitaires, l'initialisation... avec Riverpod, plus de galère avec le context.
  • Tu veux une architecture plus moderne et immutable: Riverpod pousse vers l'immutabilité avec StateNotifier et les patterns modernes. C'est plus safe que le mutable ChangeNotifier de Provider.
  • Tu veux facilement mocker tes providers pour les tests: Riverpod a été conçu avec le testing en tête. Override tes providers en tests, c'est trivial.

Evite Riverpod si:

  • Tu débutes avec Flutter: Riverpod, c'est plus abstrait que Provider. Si tu découvres Flutter ET le state management en même temps, commence par Provider. C'est plus simple à appréhender.
  • Ton équipe n'est pas à l'aise avec les concepts avancés: Riverpod demande de comprendre StateNotifier, AsyncValue, les différents types de providers... Si ton équipe galère déjà avec Provider, Riverpod va les perdre.
  • Tu veux une architecture très stricte avec Events/States: Riverpod te donne de la liberté. Si tu veux quelque chose de plus rigide et structuré (genre Bloc), Riverpod n'est peut-être pas le bon choix.

👉🏻 En résumé sur Riverpod : c’est Provider 2.0. Plus moderne, plus safe, plus flexible. Si tu es à l’aise avec Provider et que tu veux passer au niveau supérieur, Riverpod est un excellent choix.

Bloc/Cubit : l'approche architecturale

Si Provider et Riverpod sont des outils flexibles qui te laissent organiser ton code comme tu veux, Bloc est à l’opposé : c’est une architecture stricte qui impose une façon précise de gérer l’état. Et c’est exactement son point fort (ou son point faible, selon ton point de vue).

La philosophie derrière Bloc

Bloc (Business Logic Component) repose sur un pattern bien défini :

Events → Bloc → States

C’est un flux unidirectionnel inspiré de Redux, mais adapté à Flutter. Le principe :

  • L'UI envoie des Events (actions de l'utilisateur)
  • Le Bloc reçoit ces events, exécute la logique métier, et émet de nouveaux States
  • L'UI écoute ces states et se rebuild en conséquence

C’est très déclaratif et prévisible. Tu sais toujours d’où viennent les changements d’état (d’un event), et comment l’UI doit réagir (en fonction du state). On pourrait rajouter, pour compléter notre pattern du dessus:

User Action → Event → Bloc → State → UI Update

Cette séparation stricte entre UI et logique métier, c’est la philosophie centrale de Bloc.

Events, States, et le pattern

Reprenons notre exemple du compteur.

Etape 1: Définir les events

Les events représentent toutes les actions possibles.

abstract class CounterEvent {}

class IncrementCounter extends CounterEvent {}
class DecrementCounter extends CounterEvent {}
class ResetCounter extends CounterEvent {}

C’est explicite. Pas de méthode increment() qu’on appelle directement, mais un event qu’on décompose.

Etape 2 : Définir les states

Les states représentent tous les états possibles de ton UI.

class CounterState {
  final int count;
  
  CounterState(this.count);
}

Pour un compteur, c’est simple : juste un nombre. Mais pour des cas plus complexes, tu peux évidemment avoir plusieurs states différents. Dans mon app Organizer, je gère l’état des tâches des utilisateurs avec Bloc:

abstract class TasksState {}

class TasksInitial extends TaskState {}

class TasksLoading extends TasksState {}

class TasksLoaded extends TasksState {
  final List<Task> tasks;
  const TasksLoaded(this.tasks);
}

class TasksFailure extends TasksState {
  final String message;
  const TasksFailure(this.message);
}

Chaque état est une classe séparée.

Etape 3 : Créer le Bloc

Le Bloc reçoit des events et émet des states. C’est ça le flux unidirectionnel dont on a parlé plus haut.

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    on<IncrementCounter>((event, emit) {
      emit(CounterState(state.count + 1));
    });

    on<DecrementCounter>((event, emit) {
      emit(CounterState(state.count - 1));
    });

    on<ResetCounter>((event, emit) {
      emit(CounterState(0));
    });
  }
}

Pour chaque type d’event, tu définis ce qui se passe : quelle logique exécuter, quel nouveau state émettre.

Etape 4 : Utiliser dans l’UI

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          return Scaffold(
            body: Center(
              child: Text('Compteur: ${state.count}'),
            ),
            floatingActionButton: Column(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                FloatingActionButton(
                  onPressed: () {
                    context.read<CounterBloc>().add(IncrementCounter());
                  },
                  child: Icon(Icons.add),
                ),
                SizedBox(height: 8),
                FloatingActionButton(
                  onPressed: () {
                    context.read<CounterBloc>().add(DecrementCounter());
                  },
                  child: Icon(Icons.remove),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

L’UI ne fait qu’envoyer des events et écouter des states. Elle ne contient aucune logique métier.

Cubit : la version simplifiée

Bloc propose aussi Cubit, une version allégée sans events. Tu appelles directement des méthodes.

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
  void reset() => emit(0);
}

Et dans l’UI :

onPressed: () {
  context.read<CounterCubit>().increment();
}

C’est moins verbeux que Bloc, mais toujours plus structuré que Provider. Ca peut être un bon compromis entre la flexibilité de Provider et la rigueur de Bloc.

Forces et faiblesses

Points forts de Bloc

Architecture stricte et prévisible : avec Bloc, tout le monde code de la même façon. Pas de débat sur “où mettre cette logique”, “comment organiser ce fichier”. Les Events vont là, les States là, le Bloc ici. C’est carré. Sur un gros projet avec plusieurs devs, cette rigueur est précieuse.

Séparation UI / Logique parfaite : ton UI ne fait que de l’UI. Toute la logique métier est dans le Bloc. Tu peux tester ta logique sans créer un seul widget. Tu peux changer ton UI complètement sans toucher à la logique.

Debugging et time-travel : Bloc vient avec des DevTools qui te permettent de voir tous les events et states qui passent. Tu peux même faire du time-travel debugging : revenir en arrière, rejouer des events, inspecter l’historique complet. Pour debugger un bug complexe, c’est inestimable.

Testing ultra-facile : tester un Bloc, c’est trivial. Tu envoies un event, tu vérifies le state émis. Pas besoin de widgets, pas de mocks compliqués.

Gestion de l’asynchrone native : Bloc gère très bien l’asynchrone. Tu peux facilement émettre des states intermédiaires (loading, success, error) et gérer des flows complexes.

Communauté et resources : Bloc est très populaire en entreprise. Il y a énormément de tutos, de packages complémentaires, de bonnes pratiques documentées. Si tu bloques, tu trouves toujours de l’aide.

Points faibles de Bloc

Boilerplate massif : Bloc, c’est verbeux. Pour un simple compteur, tu dois créer une classe pour chaque event (IncrementCounter, DecrementCounter…), une ou plusieurs classes pour les states, le Bloc lui-même, le BlocProvider dans l’UI, le BlocBuilder pour écouter. Pour un side project simple, c’est overkill. Tu passes plus de temps à créer du boilerplate qu’à coder ta feature.

Courbe d’apprentissage raide : Bloc a beaucoup de concepts : Events, States, emit, on, BlocProvider, BlocBuilder, BlocListener, BlocConsumer… Pour quelqu’un qui débute, c’est intimidant.

Rigide : La rigueur de Bloc, c’est aussi sa limite. Parfois, tu veux juste faire un truc simple et rapide, et Bloc te force à créer un event, un state, un handler… ça peut être lourd.

Performance si mal utilisé : si tu émets trop de states, ou si tu rebuilds trop de widgets, Bloc peut avoir des problèmes de performance. Il faut bien utiliser BlocBuilder avec des buildWhen pour optimiser.

Cubit vs Bloc : L fait qu’il existe deux approches (Bloc avec events, Cubit sans events) peut créer de la confusion. Quelle approche utiliser ? Quand ? Comme souvent, ça dépend, mais ça peut créer une certaine confusion.

Quand ça vaut le coup (et quand c'est overkill)

Bloc est top si:

  • Tu as une grosse app d'entreprise avec plusieurs devs : c'est l'usage principal de Bloc. Quand tu as une équipe de 5-10 devs, que ton app a 50+ écrans, que tu veux une architecture uniforme et maintenable, Bloc est un excellent choix. La rigueur imposée devient un atout. Tout le monde code pareil, les PR sont plus faciles à review, les nouveaux devs s'intègrent plus vite.
  • Tu veux séparer strictement UI et logique : si pour toi, la séparation des responsabilités est cruciale, Bloc l'impose naturellement. Ton UI est vraiment juste de l'UI.
  • Tu as besoin de debugging avancé : le time-travel debugging et les DevTools de Bloc sont vraiment puissants. Si tu travailles sur une app complexe avec des bugs difficiles à reproduire, ces outils peuvent te sauver la vie.
  • Tu veux une architecture éprouvée en prod : Bloc est utilisé par des milliers d'apps en production, y compris de grosses entreprises. L'architecture a fait ses preuves. Si tu veux quelque chose de safe et testé, c'est un bon choix.
  • Ton équipe a déjà de l'expérience avec Redux ou des patterns similaires : si tu viens du web avec Redux, ou si ton équipe connaît déjà des architectures à base d'events/states, Bloc sera naturel. C'est le même mindset.

Quand éviter Bloc:

  • Side project ou petite app : pour une app de 5 écrans avec peu de logique, Bloc est overkill. Tu vas passer plus de temps à créer du boilerplate qu'à développer des features. Provider ou Riverpod sont amplement suffisants.
  • Tu développes seul et tu veux aller vite : la vitesse de développement avec Bloc est plus lente qu'avec Provider/Riverpod. Si tu es solo et que tu veux shipper vite, Bloc va te ralentir.
  • Tu débutes avec Flutter : apprendre Flutter ET Bloc en même temps, c'est un sacré programme. Commence par setState(), puis Provider, puis éventuellement Bloc si tu en as vraiment besoin.
  • Tu n'aimes pas le boilerplate : si écrire 5 classes pour un simple compteur te fait grincer des dents, Bloc n'est pas fait pour toi. Tu vas passer ton temps à râler sur la verbosité.

👉🏻 En résumé sur Bloc : c’est l’architecture entreprise par excellence. Rigoureux, structuré, prévisible, testable. Parfait pour les grosses équipes et les grosses apps. Mais peut être overkill pour des petits projets ou des devs solo qui veulent aller vite. Si tu hésites, commence par Provider/Riverpod, et passe à Bloc si ton projet grandit et que tu sens le besoin d’une architecture plus stricte. A moins que tu sois fan de l’architecture et philosophie de Bloc (ce qui est mon cas 🙂).

GetX : la solution controversée

GetX, c’est pas juste un state management. C’est une suite complète : gestion d’état, navigation, injection de dépendances, internationalisation, validation de formulaires, gestion de thèmes… Tout-en-un. Et c’est exactement ça qui fait débat.

La philosophie

GetX a été créé avec une philosophie simple : tout doit être simple et rapide. Pas de boilerplate, pas de verbosité, pas de BuildContext à traîner partout. Tu veux gérer de l’état ? Une ligne. Tu veux naviguer ? Une ligne. Tu veux une dépendance ? Une ligne.

L’idée, c’est de rendre Flutter encore plus productif en supprimant toute la friction.

On change pas une équipe qui gagne, reprenons notre compteur:

// Créer un controller
class CounterController extends GetxController {
  var count = 0.obs;  // .obs = observable
  
  void increment() => count++;
}

// L'utiliser dans un widget
class CounterPage extends StatelessWidget {
  final CounterController controller = Get.put(CounterController());
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Obx(() => Text('Compteur: ${controller.count}')),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

C’est tout. Pas de Provider, pas de BlocProvider, pas de Consumer. Juste Get.put() pour injecter le controller, et Obx() pour écouter les changements. On est clairement sur du minimalisme.

Navigation sans context

// Avec Flutter classique
Navigator.of(context).push(MaterialPageRoute(builder: (_) => NextPage()));

// Avec GetX
Get.to(NextPage());

Plus besoin de BuildContext. Plus besoin de MaterialPageRoute. Une ligne, c’est fait.

Dépendances globales

Get.put(ApiService());  // Inject une fois
// Utilise partout
final api = Get.find<ApiService>();

Pas de Provider à wrapper, pas de ref à passer. C’est global, c’est accessible partout.

Ce qui fait débat (magie, coupling)

Bon, maintenant parlons des vraies critiques. Parce que GetX n’est pas populaire par hasard, mais il n’est pas détesté par hasard non plus.

La "magie" qui cache la complexité

GetX fait beaucoup de choses automatiquement derrière le rideau. Quand tu fais Get.put(), GetX gère automatiquement le lifecycle, le dispose, la mémoire. Quand tu utilises .obs, GetX gère automatiquement les listeners.

Pour certains, c’est génial : ça marche tout seul, pas besoin de s’en préoccuper. Pour d’autres, c’est terrifiant : tu ne sais pas vraiment ce qui se passe sous le capot. Et quand ça bugge (et ça arrive), c’est l’enfer à debugger.

Le coupling fort

GetX s’injecte partout dans ton code. Une fois que tu l’utilises, tu es dépendant de GetX pour la navigation, pour l’état, pour les dépendances… Si demain tu veux migrer vers une autre solution, c’est la galère.

Avec Provider ou Bloc, ta logique métier est indépendante du state management. Avec GetX, tout est couplé à GetxController, Get.put(), Get.find(). Certains parlent de vendor lock-in, c’est à dire de dépendance au fournisseur: une situation où un client (en l’occurence, nous, les devs) est captif d’un fournisseur (en l’occurence GetX), rendant le changement de solution très coûteux, difficile ou risqué.

Est-ce toujours un argument à l’ère de l’IA ? Vous avez 4 heures 🙃.

Les singletons déguisés

Petit rappel pour commencer: un singleton, c’est un pattern de programmation où tu garantis qu’une classe n’aura qu’une seule instance dans toute ton application. Une seule, jamais plus. Et un singleton global, c’est un singleton qui est accessible de n’importe où dans ton code.

Pourquoi on parle de ça ? Et bien parce que Get.put() et Get.find() créent essentiellement des singletons globaux. Et les singletons, c’est un anti-pattern connu : difficiles à tester, état global mutable, dépendances cachées…

Bon ça peut paraître abstrait dit comme ça, alors pour illuster une partie du problème, imagine: avec un singleton global, tu as l’état partagé partout dans ton app

class UserService extends GetxController {
  var currentUser = User().obs;
  
  void login(User user) {
    currentUser.value = user;
  }
}

// N'importe où dans ton app
Get.find<UserService>().login(newUser);

Le problème ? N’importe quelle partie de ton code peut modifier cet état. Si tu as un bug, par exemple l’utilisateur se déconnecte tout seul, omment tu sais quelle partie du code a causé ça ? Ça peut être n’importe où. C’est ça, l’état global mutable, et c’est pour ça qu’on parle d’anti-pattern (c’est-à-dire, l’inverse d’une bonne pratique).

La philosophie "Swiss Army knife"

GetX veut tout faire : state management, navigation, dépendances, internationalisation, validation, storage, responsive design, animations… C’est un couteau suisse géant.

La question est : est-ce qu’on a vraiment besoin d’un package tiers pour tout remplacer ? Beaucoup de devs préfèrent utiliser les solutions natives de Flutter et des packages spécialisés.

Forces et Faiblesses

Points forts de GetX

Productivité : si tu veux shipper vite, GetX est imbattable. Tu codes à une vitesse folle. Pas de boilerplate, pas de verbosité, tout est simple et direct. Pour un hackathon, un POC, un side project où tu veux juste valider une idée vite fait, GetX est génial.

Courbe d’apprentissage douce : GetX est facile à apprendre. Pas de concepts compliqués comme les Events/States de Bloc, pas de différents types de providers à comprendre comme Riverpod. Tu mets .obs sur tes variables, tu wrap avec Obx(), et ça marche. Pour quelqu’un qui débute, c’est vraiment accessible.

Tout-en-un : tu n’as qu’un seul package à installer. Navigation, state, dépendances, tout est là. Pas besoin de chercher 10 packages différents et de les faire marcher ensemble.

Performance : GetX est optimisé pour la performance. Les rebuilds sont très ciblés (seul le widget dans Obx() rebuild), et GetX fait du smart disposal automatique.

Pas de BuildContext : Plus besoin de passer le context partout. C’est confortable, surtout pour la navigation et l’accès aux dépendances.

Communauté active : GetX a une grosse communauté (surtout en Amérique Latine et en Asie). Beaucoup de tutos, de packages complémentaires, de support.

Points faibles de GetX

La boîte noire : GetX fait beaucoup de choses automatiquement. C’est bien jusqu’à ce que ça casse, et là tu ne sais pas par où commencer. Le debugging peut être un cauchemar.

Coupling fort et vendor lock-in : une fois que tu utilises GetX, tu es marié avec. Migrer vers une autre solution demande de refactorer toute ton app.

Anti-patterns : GetX encourage des pratiques que beaucoup considèrent comme des anti-patterns : singletons globaux, état mutable partout, dépendances implicites… Pour un dev expérimenté qui valorise l’architecture propre, GetX fait grincer des dents.

Testing compliqué : tester du code GetX n’est pas si simple. Les dépendances globales rendent les tests unitaires plus compliqués. Il faut setup et cleanup GetX entre chaque test.

Débats et toxicité : la communauté GetX peut être… intense. Il y a eu des débats très houleux entre fans de GetX et le reste de la communauté Flutter. Certains packages refusent même de supporter GetX par principe. Cette toxicité a nui à la réputation de GetX.

Over-engineering pour certains usages : GetX fait tellement de choses que tu finis par utiliser 10% de ses features. Tu installes un package massif juste pour le state management alors que tu n’utilises ni sa navigation, ni ses utils, ni son internationalisation.

Quand l'utiliser

GetX est top pour:

  • un side project ou prototype rapide : si tu veux juste tester une idée vite fait, shipper un MVP en weekend, ou participer à un hackathon, GetX te fait gagner un temps fou. La productivité prime sur l'architecture propre.
  • du développement solo où la vitesse compte : si tu es solo dev sur un projet perso, que personne ne va maintenir ton code à part toi, et que tu veux juste que ça marche, GetX est un bon choix.
  • un débutant Flutter pour qui le state management fait peur : GetX est tellement simple que c'est accessible aux débutants. Par contre, attention, tu risques de prendre de mauvaises habitudes qu'il faudra désapprendre plus tard.
  • un petite/moyenne app : pour une app de 10-20 écrans sans logique métier hyper complexe, GetX fait le job. Le coupling et les anti-patterns ne vont pas te tuer sur un projet de cette taille.
  • un dev mobile natif (iOS/Android) : si tu as fait du dev iOS/Android, GetX ressemble un peu aux patterns de ces plateformes (singletons, accès global). Tu te sentiras peut-être plus à l'aise qu'avec Provider ou Bloc.

Quand éviter GetX:

  • Grosse app d'entreprise : si tu travailles sur une app avec une équipe de plusieurs devs, des standards de code stricts, et une volonté d'architecture propre, GetX va créer plus de problèmes qu'il n'en résout.
  • Tu valorises l'architecture et les bonnes pratiques : si pour toi, l'injection de dépendances propre, la séparation des responsabilités, et l'évitement des singletons sont importants, GetX va à l'encontre de ces principes.
  • Tu veux du code facilement testable : tester du code GetX est possible, mais c'est plus compliqué qu'avec Provider, Riverpod ou Bloc. Si le testing est une priorité, choisis autre chose.
  • Tu dois collaborer avec des devs expérimentés : beaucoup de devs Flutter expérimentés refusent catégoriquement de travailler avec GetX. Si tu rejoins une équipe ou que tu veux recruter, GetX peut être un frein.
  • Tu veux utiliser les outils standards de Flutter : si tu préfères rester sur l'écosystème standard de Flutter (navigation 2.0, Provider recommandé par Google, etc.), GetX va à contre-courant.

Les autres solutions (mentions rapides)

Bon, on a couvert les grosses solutions (Provider, Riverpod, Bloc, GetX), mais il existe plein d’autres options dans l’écosystème Flutter. Je ne vais pas les détailler autant, mais voici un rapide tour d’horizon pour que tu saches qu’elles existent.

MobX

MobX vient du monde JavaScript/React et a été porté sur Flutter. C’est basé sur la programmation réactive avec des observables et des reactions.

class Counter = _Counter with _$Counter;

abstract class _Counter with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

Pourquoi c’est cool

Si tu viens du monde React avec MobX, tu te sentiras à l’aise. C’est puissant et élégant une fois que tu maîtrises.

Pourquoi je n’en parle pas plus

Ça demande du code generation (tu dois lancer build_runner pour générer du code), ce qui ajoute une étape de build. Et la communauté Flutter autour de MobX est beaucoup plus petite que Provider ou Bloc. Moins de tutos, moins de support.

Quand l’utiliser

Si tu as déjà de l’expérience MobX dans d’autres langages et que tu veux retrouver ces patterns. Sinon, Riverpod ou Bloc sont probablement de meilleurs choix.

Redux

Redux, c’est le grand classique du monde web (React). C’est un store global avec des actions et des reducers.

class IncrementAction {}

int counterReducer(int state, dynamic action) {
  if (action is IncrementAction) {
    return state + 1;
  }
  return state;
}

final store = Store<int>(counterReducer, initialState: 0);

Pourquoi c’est cool

Redux est ultra-structuré et prévisible. Tout passe par le store, tout est tracé. C’est parfait pour des apps où tu veux un contrôle total sur chaque changement d’état.

Pourquoi je n’en parle pas plus

Redux sur Flutter, c’est verbeux. Vraiment verbeux. Pour chaque action, tu dois créer une classe, un reducer, connecter tout ça… Bloc fait essentiellement la même chose mais avec une API plus adaptée à Flutter. Et franchement, la communauté Flutter a largement adopté Bloc plutôt que Redux. Moins de ressources, moins de packages compatibles.

Quand l’utiliser

Si tu viens du monde React/Redux et que tu veux absolument retrouver ces patterns. Sinon, Bloc est probablement un meilleur choix pour Flutter.

InheritedWidget

InheritedWidget, c’est le mécanisme natif de Flutter pour propager des données dans l’arbre de widgets. C’est ce que Provider utilise sous le capot.

class MyInheritedWidget extends InheritedWidget {
  final int data;
  
  MyInheritedWidget({required this.data, required Widget child}) 
      : super(child: child);

  static MyInheritedWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
  }

  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return data != oldWidget.data;
  }
}

Pourquoi c’est cool

C’est natif, pas de dépendance externe. Et c’est éducatif de comprendre comment ça marche un peu plus bas.

Pourquoi je n’en parle pas plus

C’est verbeux et bas niveau. Tu dois gérer manuellement plein de choses que Provider ou Riverpod font pour toi. Personne n’utilise InheritedWidget directement en prod, c’est juste la base sur laquelle les autres solutions sont construites.

Quand l’utiliser

Pour apprendre comment fonctionne la propagation d’état dans Flutter. Ou si tu veux vraiment un contrôle total sans dépendances externes (mais soyons clair, tu n’en as probablement jamais besoin). Sinon, utilise Provider qui wrap InheritedWidget proprement.

Signals (nouveau venu)

Signals est un pattern de réactivité qui vient du monde JavaScript (SolidJS, Preact) et qui commence à arriver sur Flutter.

final counter = signal(0);

// Dans ton widget
Text('${counter.value}')

// Modifier
counter.value++;

Pourquoi c’est cool

C’est simple et très performant. Les rebuilds sont super ciblés, et la syntaxe est minimale.

Pourquoi je n’en parle pas plus

C’est très récent sur Flutter. L’écosystème est encore jeune, peu de packages, peu de retours d’expérience en prod. Ça a l’air prometteur, mais c’est peut être un peu tôt pour recommander ça les yeux fermés.

Quand l’utiliser

Si tu es du genre early adopter et que tu veux expérimenter avec les nouveaux patterns. Ou dans quelques années quand ça aura mûri. Pour l’instant, mon conseil est de rester sur Provider/Riverpod/Bloc.

Pourquoi je me concentre sur Provider/Riverpod/Bloc/GetX

Parce que ce sont les 4 solutions dominantes dans la communauté Flutter en 2026:

  • Provider : recommandé par Google, ultra-populaire
  • Riverpod : la version moderne de Provider, en forte croissance
  • Bloc : le standard en entreprise
  • GetX : controversé mais très utilisé

Toutes les autres solutions (MobX, Redux, Signals…) ont des niches, mais représentent une part beaucoup plus petite de l’écosystème. Moins de tutos, moins de packages compatibles, moins de jobs qui les demandent.

Mon conseil ? Ne te perds pas dans les options. Choisis une des 4 principales selon ton contexte :

  • Débutant ou petit projet → Provider
  • Expérience avec Provider et tu veux allez plus loin → Riverpod
  • Grosse app d'entreprise → Bloc
  • Side project et vitesse pure → GetX (avec précaution)

Conclusion : mes recommandations 2026

Bon, après ce tour d’horizon complet, il est temps de trancher. Quelle solution choisir en 2026 ?

Si tu débutes : commence par Provider

Si tu découvres Flutter et le state management, commence par Provider.

Pourquoi ? Parce que c’est :

  • Simple à comprendre : ChangeNotifier, notifyListeners(), et c'est tout
  • Recommandé officiellement par l'équipe Flutter
  • Bien documenté avec des tonnes de tutos partout
  • Une transition naturelle depuis setState()

Avec Provider, tu apprends les concepts fondamentaux du state management (séparer l’état de l’UI, partager des données, écouter les changements) sans te noyer dans la complexité.

Et le bonus : fois que tu maîtrises Provider, tu comprends déjà 80% de Riverpod. La migration est facile si tu veux upgrader plus tard.

Commence simple. Tu complexifieras plus tard si besoin.

Si tu veux scaler : pars sur Riverpod ou Bloc

Ton app commence à grandir ? Tu passes de 5 à 20+ écrans ? Tu sens que Provider devient limité ? Il est temps d’upgrader.

Choisis Riverpod si :

  • Tu développes seul ou en petite équipe (2-3 devs)
  • Tu veux de la flexibilité dans l'architecture
  • La compile-time safety et AsyncValue te font de l'œil
  • Tu veux quelque chose de moderne et bien pensé

Riverpod, c’est l’évolution naturelle de Provider. Plus puissant, plus safe, mais toujours flexible.

Choisis Bloc si :

  • Tu travailles en équipe (5+ devs)
  • Tu veux une architecture stricte et uniforme
  • Le testing rigoureux et le time-travel debugging sont importants
  • Tu valorises la séparation UI/logique absolue

Bloc, c’est le choix entreprise. Plus verbeux, plus rigide, mais aussi plus structuré. Si ton app va devenir grosse et complexe, Bloc te donnera un cadre solide pour ne pas partir en cacahuètes.

Ce que j'utilise vraiment dans mes apps

Bon je vais peut-être te surprendre sur ce coup, mais moi, concrètement, j’utilise Bloc.

Pourtant, je développe seule, sur des petites apps, je n’ai pas beaucoup d’expérience en Flutter et malgré ça, j’ai choisi Bloc qui est réputé pour les grosses équipes et les grosses apps.

Pourquoi ?

Parce que j’aime la rigueur et la structure que Bloc impose. Quand je reviens sur mon code, je sais exactement où tout se trouve. Les events vont là, les states là, la logique ici. Et franchement, le boilerplate ne me dérange pas. Oui, je crée plus de fichiers qu’avec Provider ou Riverpod. Mais en échange, j’ai :

  • Une séparation UI/logique parfaite
  • Du code facilement testable
  • Une architecture claire qui ne part pas dans tous les sens

C’est certainement la matheuse en moi qui parle, mais pour moi, ça vaut le coup. Je préfère avoir un peu plus de code mais bien organisé, qu’un code plus court mais où je dois chercher partout pour comprendre le flow.

Mais attention : je ne dis pas que Bloc est le meilleur choix pour tout le monde. C’est mon choix parce que ça correspond à ma façon de coder et à mes priorités.

Si tu valorises la vitesse pure et que le boilerplate te frustre, Riverpod est probablement mieux pour toi.

Si tu veux juste que ça marche sans te prendre la tête, Provider fait le job.

L’important, c’est de choisir ce qui te convient, pas ce qui est objectivement “le meilleur”.

Le piège à éviter : ne pas overthink

Bon, et voici le conseil le plus important de tout cet article : arrête de réfléchir et commence à coder.

Sérieusement.

Je vois tellement de développeurs (et j’ai été cette personne) qui passent des semaines à comparer les solutions de state management, à lire des articles, à regarder des tutos “Provider vs Bloc vs Riverpod”, à demander sur Reddit quelle est la meilleure solution… Et pendant ce temps, leur app n’avance pas.

Et pourtant, la vérité c’est qu’il n’y a pas de mauvais choix. Toutes ces solutions fonctionnent. Toutes sont utilisées par des milliers d’apps en production. Toutes ont des success stories.

Le vrai mauvais choix, c’est de ne pas choisir du tout.

Voici ce que tu devrais faire :

  • Lis cet article (check, tu viens de le faire)
  • Choisis une solution
  • Code pendant 2 semaines avec cette solution
  • Si ça te convient, continue. Si ça ne te convient pas, change.

C’est aussi simple que ça.

Et oui, changer de state management, c’est du refactoring. Mais sur une petite app de 2 semaines ET avec l’IA, c’est vraiment pas la fin du monde.

Par contre, passer 1 mois à comparer des solutions sans coder, ça c’est du temps perdu définitivement 🙃

Voilà, j’espère que cet article t’a aidé à y voir plus clair dans la jungle du state management Flutter. N’hésite pas à m’écrire si tu n’es pas d’accord sur un point ou si tu veux simplement discuter de ton choix !

Et maintenant, ferme cet onglet et va coder !

À la prochaine !

Commentaires