Gestion d'état dans Flutter
Introduction à la gestion d’état
Section titled “Introduction à la gestion d’état”La gestion d’état est un aspect fondamental du développement d’applications Flutter. Elle détermine comment votre application réagit aux interactions utilisateur et maintient la cohérence des données à travers les différents écrans.
Widgets avec état (StatefulWidget)
Section titled “Widgets avec état (StatefulWidget)”Création d’un StatefulWidget
Section titled “Création d’un StatefulWidget”Un StatefulWidget est un widget qui peut changer d’état au cours de sa durée de vie. Contrairement aux StatelessWidget, ils peuvent maintenir des données mutables.
import 'package:flutter/material.dart';
void main() { runApp(const MaterialApp(home: MainScreen()));}
class MainScreen extends StatefulWidget { const MainScreen({Key? key}) : super(key: key);
@override State<MainScreen> createState() => _MainScreenState();}
class _MainScreenState extends State<MainScreen> { int value = 0;
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), floatingActionButton: FloatingActionButton( onPressed: () => setState(() { value++; }), child: const Icon(Icons.add), ), body: Center( child: Text('$value'), ), ); }}Cycle de vie des widgets
Section titled “Cycle de vie des widgets”Méthodes du cycle de vie
Section titled “Méthodes du cycle de vie”Les StatefulWidget disposent de plusieurs méthodes de cycle de vie qui permettent de contrôler leur comportement :
class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin { final tabs = const [ Tab(text: 'Livres'), Tab(text: 'Films'), Tab(text: 'Disques'), ];
late TabController tabController;
@override void initState() { // Appelée une seule fois lors de la création du widget tabController = TabController( length: tabs.length, initialIndex: 0, vsync: this, ); super.initState(); }
@override void dispose() { // Appelée lors de la destruction du widget tabController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( bottom: TabBar( controller: tabController, tabs: tabs, isScrollable: true, ), ), body: TabBarView( controller: tabController, children: const <Widget>[ Center(child: Text('Livres')), Center(child: Text('Films')), Center(child: Text('Musique')), ], ), ); }}Principales méthodes du cycle de vie
Section titled “Principales méthodes du cycle de vie”initState(): Appelée une seule fois lors de la création du widgetbuild(): Appelée chaque fois que le widget doit être reconstruitdispose(): Appelée lors de la destruction du widget pour libérer les ressources
Limitations de setState
Section titled “Limitations de setState”L’utilisation de setState présente plusieurs limitations :
- Séparation Vue/Logique difficile : Le code métier est mélangé avec l’interface utilisateur
- Testabilité réduite : Difficile de tester la logique indépendamment de l’interface
- Scalabilité limitée : Devient rapidement complexe dans de grandes applications
- Performance : Peut provoquer des reconstructions inutiles
L’utilisation de setState rend difficile la séparation Vue/Logique et par conséquent la testabilité.
De nombreux packages tentent de proposer une solution pour organiser les applications Flutter, en s’inspirant de différents patterns :
- Bloc : stream et réactivité
- Provider : notifiers et réactivité
- Mobx : Réactivité et ViewModel
- Redux : global store & unidirectional data flow
ValueNotifier et ChangeNotifier
Section titled “ValueNotifier et ChangeNotifier”Le pattern Flutter : Notifiers
Section titled “Le pattern Flutter : Notifiers”Flutter propose deux classes intégrant un mécanisme d’Observateur :
- Un observable est un objet auquel peuvent s’abonner des “listeners” (aka observateurs)
- Lorsqu’un changement a lieu, l’observable “notifie” ses listeners
ValueNotifier
Section titled “ValueNotifier”ValueNotifier permet de définir un objet observable que des widgets pourront “surveiller” et être prévenus des modifications.
class MainScreen extends StatelessWidget { final notifier = ValueNotifier(0);
MainScreen({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: Column( children: [ ValueListenableBuilder( valueListenable: notifier, builder: (context, value, child) { return Text('$value'); }, ), ElevatedButton( onPressed: () => notifier.value += 1, child: const Icon(Icons.add), ) ], ), ), ); }}Étendre ValueNotifier
Section titled “Étendre ValueNotifier”Vous pouvez créer vos propres classes en étendant ValueNotifier :
class CounterController extends ValueNotifier<int> { CounterController(int value) : super(value);
void increment() => value++; void decrement() => value--;}ChangeNotifier
Section titled “ChangeNotifier”ChangeNotifier est un objet observable dont plusieurs propriétés peuvent être “mutables”. À chaque modification, il doit explicitement appeler notifyListeners().
class AsyncCounterViewModel extends ChangeNotifier { final CounterService service;
AsyncCounterViewModel(this.service) { init(); }
Future<void> init() async { _isLoading = true; notifyListeners(); _count = await service.load(); _isLoading = false; notifyListeners(); }
int? _count; int? get count => _count;
bool _isLoading = false; bool get isLoading => _isLoading;
String? _errorText; bool get hasError => _errorText != null; String? get errorText => _errorText;
void increment() async { _isLoading = true; notifyListeners(); final newValue = await service.increment(); _count = newValue; _isLoading = false; notifyListeners(); }
void decrement() async { _isLoading = true; notifyListeners(); final newValue = await service.decrement(); _count = newValue; _isLoading = false; notifyListeners(); }}Patterns de gestion d’état
Section titled “Patterns de gestion d’état”Architecture recommandée
Section titled “Architecture recommandée”Pour une architecture robuste, il est recommandé de séparer :
- Présentation : Les widgets d’interface utilisateur
- Logique métier : Les contrôleurs et services
- Données : Les modèles et repositories
Patterns populaires
Section titled “Patterns populaires”- Observer Pattern : Utilisé par ValueNotifier et ChangeNotifier
- Repository Pattern : Pour l’accès aux données
- Service Locator : Via les InheritedWidget et Provider
- MVVM : Model-View-ViewModel avec ChangeNotifier
Package Provider
Section titled “Package Provider”Avantages de Provider
Section titled “Avantages de Provider”Provider présente plusieurs avantages :
- Utilise les mécanismes natifs de Flutter :
InheritedWidget - Moins verbeux que Bloc
- Pas de génération de code
- Intégration naturelle avec l’écosystème Flutter
Mécanisme InheritedWidget
Section titled “Mécanisme InheritedWidget”Ce mécanisme permet à un widget de retrouver l’instance d’un widget “parent” dans l’arbre de widgets. Cela permet d’éviter de longues chaînes d’injection de paramètres.
Exemples d’utilisation native :
MediaQuery.of(context)Navigator.of(context)Theme.of(context)
Déclarer un Provider
Section titled “Déclarer un Provider”Les providers proposent deux constructeurs principaux :
Provider.value()
Section titled “Provider.value()”Pour utiliser un objet existant :
class App extends StatelessWidget { const App({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { final user = User('joe@mail.com'); return Provider.value( value: user, child: const MaterialApp(home: MainScreen()), ); }}Provider()
Section titled “Provider()”Pour instancier un nouvel objet :
class App extends StatelessWidget { const App({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return Provider( create: (context) => UserService(), child: const MaterialApp(home: MainScreen()), ); }}Accéder à un Provider
Section titled “Accéder à un Provider”Deux syntaxes sont disponibles :
- Syntaxe classique :
Provider.of<AuthService>(context); - Extension de méthodes :
context.read<AuthService>();
class MainScreen extends StatelessWidget { const MainScreen({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { final user = context.read<User>(); return Scaffold( appBar: AppBar(title: Text(user.mail)), body: const MainView(), ); }}Types de Providers spécialisés
Section titled “Types de Providers spécialisés”ChangeNotifierProvider: pour lesChangeNotifieret lesValueNotifierStreamProvider: pour les flux de donnéesFutureProvider: pour les opérations asynchronesProxyProvider: pour des objets dépendants d’autres objets providésProvider: pour les données statiquesMultiProvider: pour réduire l’imbrication
MultiProvider
Section titled “MultiProvider”Pour déclarer plusieurs Providers en une fois :
MultiProvider( providers: [ Provider.value(value: userService), Provider.value(value: messageService), ], child: widget.child,);ChangeNotifierProvider avec rebuild automatique
Section titled “ChangeNotifierProvider avec rebuild automatique”Les ChangeNotifierProvider surveillent les changements et provoquent le rebuild automatique des widgets dépendants :
class App extends StatelessWidget { const App({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => TimeController(), child: const MaterialApp(home: MainScreen()), ); }}
class MainScreen extends StatelessWidget { const MainScreen({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { final timeController = context.watch<TimeController>(); return Scaffold( appBar: AppBar( title: Text(timeController.value.toIso8601String()), ), body: const MainView(), ); }}ProxyProvider
Section titled “ProxyProvider”Pour définir des dépendances entre Providers :
MultiProvider( providers: [ ChangeNotifierProvider.value(value: authController), ProxyProvider<AuthController, FeedService>( update: (_, controller, service) => FeedService(FeedClient(controller.user?.token)), lazy: true, ), ], child: widget.child,);Concepts avancés de gestion d’état
Section titled “Concepts avancés de gestion d’état”Sealed Classes pour la gestion d’état asynchrone
Section titled “Sealed Classes pour la gestion d’état asynchrone”sealed class AsyncTask<T> {}
class PendingTask<T> extends AsyncTask<T> {}
class TaskError<T> extends AsyncTask<T> { final String error; TaskError([this.error = 'Error !']);}
class AsyncResult<T> extends AsyncTask<T> { final T data; AsyncResult(this.data);}Alternatives à Provider
Section titled “Alternatives à Provider”- InheritedWidget : Solution native Flutter
- Bloc : Pattern stream-based
- Riverpod : Version améliorée de Provider
- MobX : Gestion d’état réactive
Bonnes pratiques
Section titled “Bonnes pratiques”- Séparez la logique de l’interface : Utilisez des contrôleurs ou des ViewModels
- Testez votre logique métier : Indépendamment des widgets
- Minimisez les rebuilds : Utilisez
context.read()pour les actions,context.watch()pour l’écoute - Gérez le cycle de vie : Disposez correctement des ressources
- Organisez vos providers : Utilisez une hiérarchie logique dans l’arbre de widgets
Conclusion
Section titled “Conclusion”La gestion d’état dans Flutter évolue d’une approche simple avec setState vers des solutions plus sophistiquées comme Provider. Le choix de la solution dépend de la complexité de votre application :
- Applications simples :
setStateetStatefulWidget - Applications moyennes :
ValueNotifieretChangeNotifier - Applications complexes : Provider, Bloc, ou Riverpod
L’important est de maintenir une architecture claire qui sépare la logique métier de l’interface utilisateur pour faciliter la maintenance et les tests.