Skip to content

Gestion d'état dans Flutter

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.

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'),
),
);
}
}

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')),
],
),
);
}
}
  • initState() : Appelée une seule fois lors de la création du widget
  • build() : Appelée chaque fois que le widget doit être reconstruit
  • dispose() : Appelée lors de la destruction du widget pour libérer les ressources

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

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 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),
)
],
),
),
);
}
}

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 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();
}
}

Pour une architecture robuste, il est recommandé de séparer :

  1. Présentation : Les widgets d’interface utilisateur
  2. Logique métier : Les contrôleurs et services
  3. Données : Les modèles et repositories
  • 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

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

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)

Les providers proposent deux constructeurs principaux :

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()),
);
}
}

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()),
);
}
}

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(),
);
}
}
  • ChangeNotifierProvider : pour les ChangeNotifier et les ValueNotifier
  • StreamProvider : pour les flux de données
  • FutureProvider : pour les opérations asynchrones
  • ProxyProvider : pour des objets dépendants d’autres objets providés
  • Provider : pour les données statiques
  • MultiProvider : pour réduire l’imbrication

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(),
);
}
}

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,
);

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);
}
  1. Séparez la logique de l’interface : Utilisez des contrôleurs ou des ViewModels
  2. Testez votre logique métier : Indépendamment des widgets
  3. Minimisez les rebuilds : Utilisez context.read() pour les actions, context.watch() pour l’écoute
  4. Gérez le cycle de vie : Disposez correctement des ressources
  5. Organisez vos providers : Utilisez une hiérarchie logique dans l’arbre de widgets

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 : setState et StatefulWidget
  • Applications moyennes : ValueNotifier et ChangeNotifier
  • 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.