6 façons de gérer l'état dans les applications Flutter (avec des exemples de code)

Comment gérer l'état dans Flutter

Sommaire

Dans cet article, nous allons explorer six méthodes populaires de gestion de l’état dans Flutter apps, y compris des exemples concrets et des bonnes pratiques :

La gestion de l’état est l’un des sujets les plus importants - et débattus - dans le développement Flutter. Elle détermine la manière dont votre application gère les changements de données et met à jour l’interface utilisateur efficacement. Flutter vous offre plusieurs stratégies pour gérer l’état — allant de simples à hautement évolutifs.

Les stratégies de gestion de l’état dans Flutter sont :

  1. 🧱 setState() — La méthode intégrée, la plus simple
  2. 🏗️ InheritedWidget — La fondation de Flutter pour la propagation de l’état
  3. 🪄 Provider — La solution recommandée pour la plupart des applications
  4. 🔒 Riverpod — L’évolution moderne et sécurisée à la compilation de Provider
  5. 📦 Bloc — Pour les applications évolutives et d’envergure entreprise
  6. GetX — Une solution légère et tout-en-un

flutter troubles mega tracktor


1. Utilisation de setState() — Les bases

La manière la plus simple de gérer l’état dans Flutter est d’utiliser la méthode intégrée setState() dans un StatefulWidget.

Cette approche est idéale pour l’état local de l’interface utilisateur — où l’état appartient à un seul widget et n’a pas besoin d’être partagé à travers l’application.

Exemple : Application de compteur avec setState()

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('setState Counter')),
      body: Center(
        child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Avantages

  • Très simple à mettre en œuvre
  • Idéal pour l’état local ou temporaire de l’interface utilisateur
  • Aucune dépendance externe

Inconvénients

  • Ne s’adapte pas aux grandes applications
  • Difficile de partager l’état entre les widgets
  • Logique mélangée avec l’interface utilisateur

Utilisez-le lorsque :

  • Vous prototypiez ou construisez de petits widgets
  • Vous gérez un état isolé de l’interface utilisateur (par exemple, basculer un bouton, afficher une modale)

2. InheritedWidget — La fondation de Flutter

InheritedWidget est le mécanisme de bas niveau que Flutter utilise pour propager les données dans l’arbre des widgets. La plupart des solutions de gestion de l’état (y compris Provider) sont construites dessus.

Comprendre InheritedWidget vous aide à saisir comment la gestion de l’état de Flutter fonctionne en interne.

Exemple : Gestionnaire de thème avec InheritedWidget

import 'package:flutter/material.dart';

// L'InheritedWidget qui contient les données du thème
class AppTheme extends InheritedWidget {
  final bool isDarkMode;
  final Function toggleTheme;

  const AppTheme({
    Key? key,
    required this.isDarkMode,
    required this.toggleTheme,
    required Widget child,
  }) : super(key: key, child: child);

  // Méthode d'accès pour obtenir le AppTheme le plus proche dans l'arbre des widgets
  static AppTheme? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppTheme>();
  }

  @override
  bool updateShouldNotify(AppTheme oldWidget) {
    return isDarkMode != oldWidget.isDarkMode;
  }
}

// Wrapper étatif pour gérer les changements d'état
class ThemeManager extends StatefulWidget {
  final Widget child;

  const ThemeManager({Key? key, required this.child}) : super(key: key);

  @override
  _ThemeManagerState createState() => _ThemeManagerState();
}

class _ThemeManagerState extends State<ThemeManager> {
  bool _isDarkMode = false;

  void _toggleTheme() {
    setState(() {
      _isDarkMode = !_isDarkMode;
    });
  }

  @override
  Widget build(BuildContext context) {
    return AppTheme(
      isDarkMode: _isDarkMode,
      toggleTheme: _toggleTheme,
      child: widget.child,
    );
  }
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ThemeManager(
      child: Builder(
        builder: (context) {
          final theme = AppTheme.of(context);
          return MaterialApp(
            theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
            home: HomeScreen(),
          );
        },
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final theme = AppTheme.of(context);
    
    return Scaffold(
      appBar: AppBar(title: Text('InheritedWidget Theme')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Theme: ${theme!.isDarkMode ? "Dark" : "Light"}',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => theme.toggleTheme(),
              child: Text('Toggle Theme'),
            ),
          ],
        ),
      ),
    );
  }
}

Avantages

  • Intégré à Flutter — aucune dépendance externe
  • Mises à jour efficaces des widgets
  • Fondation pour comprendre d’autres solutions
  • Contrôle direct sur la logique de propagation

Inconvénients

  • Code de boilerplate verbeux
  • Nécessite un StatefulWidget wrapper
  • Facile à faire des erreurs
  • Pas très convivial pour les débutants

Utilisez-le lorsque :

  • Vous apprenez comment la gestion de l’état de Flutter fonctionne en interne
  • Vous construisez des solutions de gestion de l’état personnalisées
  • Vous avez besoin d’un contrôle très spécifique sur la propagation de l’état

3. Provider — La solution recommandée par Flutter

Lorsque l’état de votre application doit être partagé entre plusieurs widgets, Provider vient à la rescousse.

Provider est basé sur l’inversion de contrôle — au lieu que les widgets possèdent l’état, un fournisseur le rend disponible pour d’autres widgets. L’équipe Flutter recommande officiellement Provider pour les applications de taille moyenne.

Configuration

Ajoutez ceci à votre pubspec.yaml :

dependencies:
  provider: ^6.0.5

Exemple : Application de compteur avec Provider

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterModel>();
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.read<CounterModel>().increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Avantages

  • Mises à jour réactives et efficaces
  • Séparation propre entre l’interface utilisateur et la logique
  • Bien documenté et soutenu par la communauté

Inconvénients

  • Un peu plus de boilerplate que setState()
  • Les fournisseurs imbriqués peuvent devenir complexes

Utilisez-le lorsque :

  • Vous avez besoin d’un état partagé entre plusieurs widgets
  • Vous souhaitez un modèle réactif sans complexité
  • Votre application dépasse la taille de prototype

4. Riverpod — L’évolution moderne de Provider

Riverpod est une réécriture complète de Provider qui élimine sa dépendance à BuildContext et ajoute une sécurité à la compilation. Il a été conçu pour résoudre les limites de Provider tout en conservant la même philosophie.

Configuration

Ajoutez ceci à votre pubspec.yaml :

dependencies:
  flutter_riverpod: ^2.4.0

Exemple : Profil utilisateur avec Riverpod

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Modèle
class UserProfile {
  final String name;
  final int age;
  
  UserProfile({required this.name, required this.age});
  
  UserProfile copyWith({String? name, int? age}) {
    return UserProfile(
      name: name ?? this.name,
      age: age ?? this.age,
    );
  }
}

// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
  ProfileNotifier() : super(UserProfile(name: 'Guest', age: 0));

  void updateName(String name) {
    state = state.copyWith(name: name);
  }

  void updateAge(int age) {
    state = state.copyWith(age: age);
  }
}

// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
  return ProfileNotifier();
});

// Provider calculé
final greetingProvider = Provider<String>((ref) {
  final profile = ref.watch(profileProvider);
  return 'Hello, ${profile.name}! You are ${profile.age} years old.';
});

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: ProfileScreen());
  }
}

class ProfileScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final profile = ref.watch(profileProvider);
    final greeting = ref.watch(greetingProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Profile')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(greeting, style: TextStyle(fontSize: 20)),
            SizedBox(height: 30),
            TextField(
              decoration: InputDecoration(labelText: 'Name'),
              onChanged: (value) {
                ref.read(profileProvider.notifier).updateName(value);
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Age: ${profile.age}', style: TextStyle(fontSize: 18)),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () {
                    ref.read(profileProvider.notifier).updateAge(profile.age + 1);
                  },
                  child: Text('+'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    if (profile.age > 0) {
                      ref.read(profileProvider.notifier).updateAge(profile.age - 1);
                    }
                  },
                  child: Text('-'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Avantages

  • Aucune dépendance à BuildContext — accédez à l’état n’importe où
  • Sécurité à la compilation — détectez les erreurs avant l’exécution
  • Meilleure testabilité
  • Puissante composition d’état
  • Aucun fuit de mémoire

Inconvénients

  • API différente des modèles traditionnels de Flutter
  • Courbe d’apprentissage plus raide que Provider
  • Communauté plus petite (mais en croissance rapide)

Utilisez-le lorsque :

  • Vous commencez un nouveau projet Flutter
  • Vous souhaitez une sécurité de type et des garanties à la compilation
  • Des dépendances et compositions d’état complexes
  • Vous travaillez sur des applications de taille moyenne à grande

5. BLoC (Business Logic Component) — Pour les applications d’entreprise

Le BLoC pattern est une architecture plus avancée qui sépare complètement la logique métier de l’interface utilisateur à l’aide de flux.

Il est idéal pour les applications à grande échelle ou d’entreprise, où les transitions d’état prévisibles et testables sont essentielles.

Configuration

Ajoutez cette dépendance à votre projet :

dependencies:
  flutter_bloc: ^9.0.0

Exemple : Application de compteur avec BLoC

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// Événements
abstract class CounterEvent {}
class Increment extends CounterEvent {}

// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: MaterialApp(home: CounterScreen()),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('Count: $count', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        child: Icon(Icons.add),
      ),
    );
  }
}

Avantages

  • Évolutif et maintenable pour les applications complexes
  • Séparation claire des couches
  • Facile à tester en unité

Inconvénients

  • Plus de boilerplate
  • Courbe d’apprentissage plus raide

Utilisez-le lorsque :

  • Vous construisez des projets d’entreprise ou à long terme
  • Vous avez besoin de logique prévisible et testable
  • Plusieurs développeurs travaillent sur différents modules de l’application

6. GetX — La solution légère tout-en-un

GetX est une solution de gestion de l’état minimaliste mais puissante qui inclut également le routage, l’injection de dépendances et des utilitaires. Il est connu pour avoir le moins de boilerplate de toutes les solutions.

Configuration

Ajoutez ceci à votre pubspec.yaml :

dependencies:
  get: ^4.6.5

Exemple : Liste d’achats avec GetX

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// Contrôleur
class ShoppingController extends GetxController {
  // Liste observable
  var items = <String>[].obs;
  
  // Compteur observable
  var totalItems = 0.obs;

  void addItem(String item) {
    if (item.isNotEmpty) {
      items.add(item);
      totalItems.value = items.length;
    }
  }

  void removeItem(int index) {
    items.removeAt(index);
    totalItems.value = items.length;
  }

  void clearAll() {
    items.clear();
    totalItems.value = 0;
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Shopping List',
      home: ShoppingScreen(),
    );
  }
}

class ShoppingScreen extends StatelessWidget {
  // Initialisation du contrôleur
  final ShoppingController controller = Get.put(ShoppingController());
  final TextEditingController textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Shopping List'),
        actions: [
          Obx(() => Padding(
                padding: EdgeInsets.all(16),
                child: Center(
                  child: Text(
                    'Items: ${controller.totalItems}',
                    style: TextStyle(fontSize: 18),
                  ),
                ),
              )),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: textController,
                    decoration: InputDecoration(
                      hintText: 'Enter item name',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    controller.addItem(textController.text);
                    textController.clear();
                  },
                  child: Icon(Icons.add),
                ),
              ],
            ),
          ),
          Expanded(
            child: Obx(() {
              if (controller.items.isEmpty) {
                return Center(
                  child: Text('No items in your list'),
                );
              }
              
              return ListView.builder(
                itemCount: controller.items.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    leading: CircleAvatar(child: Text('${index + 1}')),
                    title: Text(controller.items[index]),
                    trailing: IconButton(
                      icon: Icon(Icons.delete, color: Colors.red),
                      onPressed: () => controller.removeItem(index),
                    ),
                  );
                },
              );
            }),
          ),
          if (controller.items.isNotEmpty)
            Padding(
              padding: EdgeInsets.all(16),
              child: ElevatedButton(
                onPressed: () {
                  Get.defaultDialog(
                    title: 'Clear List',
                    middleText: 'Are you sure you want to clear all items?',
                    textConfirm: 'Yes',
                    textCancel: 'No',
                    onConfirm: () {
                      controller.clearAll();
                      Get.back();
                    },
                  );
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  minimumSize: Size(double.infinity, 50),
                ),
                child: Text('Clear All'),
              ),
            ),
        ],
      ),
    );
  }
}

Avantages

  • Peu de boilerplate — développement le plus rapide
  • Solution tout-en-un (état, routage, injection de dépendances, snacks, dialogues)
  • Aucun besoin de BuildContext
  • Très légère et rapide
  • Facile à apprendre

Inconvénients

  • Moins “Flutter-like” que Provider/BLoC
  • Peut entraîner une couplage étroit si on n’y fait pas attention
  • Écosystème plus petit que Provider
  • Certains développeurs le trouvent “trop magique”

Utilisez-le lorsque :

  • Vous faites un prototypage rapide ou un MVP
  • Des applications petites à moyennes
  • Vous souhaitez un minimum de boilerplate
  • Votre équipe préfère la simplicité à l’architecture stricte

Choisir la bonne solution de gestion de l’état

Solution Complexité Boilerplate Courbe d’apprentissage Meilleure pour
🧱 setState() Faible Minimale Facile État local simple, prototypes
🏗️ InheritedWidget Moyenne Élevée Moyenne Apprendre les internes de Flutter, solutions personnalisées
🪄 Provider Faible-Moyenne Faible Facile La plupart des applications, état partagé
🔒 Riverpod Moyenne Faible-Moyenne Moyenne Applications modernes, sécurité de type
📦 BLoC Élevée Élevée Raide Applications d’entreprise, logique métier complexe
GetX Faible Minimale Facile Développement rapide, MVPs

Guide de décision rapide :

Complexité de l’application Approche recommandée Cas d’utilisation
🪶 Simple setState() ou GetX Basculer des boutons, animations, petits widgets
⚖️ Moyenne Provider ou Riverpod Thèmes partagés, paramètres utilisateur, mise en cache des données
🏗️ Complexe BLoC ou Riverpod E-commerce, applications de chat, tableaux de bord financiers

Réflexions finales

Il n’y a pas de solution universelle pour la gestion de l’état dans Flutter.

Voici une approche pratique :

  • Commencez petit avec setState() pour les widgets simples et les prototypes
  • Apprenez les fondamentaux avec InheritedWidget pour comprendre comment Flutter fonctionne en interne
  • Passez à Provider lorsque votre application grandit et a besoin d’un état partagé
  • Considérez Riverpod pour les nouveaux projets où la sécurité de type et les modèles modernes comptent
  • Adoptez BLoC pour les applications d’entreprise avec une logique métier complexe
  • Essayez GetX lorsque le développement rapide et le minimum de boilerplate sont prioritaires

Points clés :

✅ Ne suringénieriez pas — utilisez setState() jusqu’à ce que vous en ayez besoin
✅ Provider et Riverpod sont de très bons choix pour la plupart des applications
✅ BLoC brille dans les grandes équipes et les applications complexes
✅ GetX est excellent pour la vitesse, mais faites attention au couplage
✅ Comprendre InheritedWidget vous aide à maîtriser toute solution

L’essentiel est de rééquilibrer simplicité, évolutivité et maintenabilité — et de choisir le bon outil pour vos besoins spécifiques, votre expertise d’équipe et vos exigences de projet.

Liens des bibliothèques de gestion de l’état de Flutter

Autres liens utiles