setState et Gestion d'état locale
Comprendre setState
setState est la méthode de base pour gérer l'état dans un StatefulWidget. Elle indique à Flutter que l'état interne a changé et que l'interface doit être reconstruite.
Exemple basique
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Compteur avec setState'),
),
body: const Center(
child: CounterWidget(),
),
),
);
}
}
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Compteur : $_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _incrementCounter,
child: const Text('Incrémenter'),
),
],
);
}
}
Règles importantes
- Toujours appeler setState pour modifier l'état
- Modifications dans setState : placer les changements d'état à l'intérieur
- Ne pas appeler setState après dispose ou pendant build
// MAUVAIS
void _increment() {
_counter++; // Ne reconstruit pas l'interface
}
// BON
void _increment() {
setState(() {
_counter++;
});
}
Cycle de vie d'un StatefulWidget
Méthodes du cycle de vie
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Lifecycle Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Cycle de vie d\'un Widget'),
),
body: const Center(
child: LifecycleDemo(),
),
),
);
}
}
class LifecycleDemo extends StatefulWidget {
const LifecycleDemo({super.key});
State<LifecycleDemo> createState() => _LifecycleDemoState();
}
class _LifecycleDemoState extends State<LifecycleDemo> {
void initState() {
super.initState();
// Appelé une seule fois à la création du widget
// Idéal pour initialiser des données, controllers, listeners
print('initState appelé');
}
void didChangeDependencies() {
super.didChangeDependencies();
// Appelé après initState et quand les dépendances changent
// Ex: Theme.of(context), MediaQuery.of(context)
print('didChangeDependencies appelé');
}
void didUpdateWidget(covariant LifecycleDemo oldWidget) {
super.didUpdateWidget(oldWidget);
// Appelé quand le widget parent reconstruit ce widget avec
// une nouvelle configuration
print('didUpdateWidget appelé');
}
void dispose() {
// Appelé quand le widget est définitivement retiré de l'arbre
// Libérer les ressources : controllers, listeners, streams
print('dispose appelé');
super.dispose();
}
Widget build(BuildContext context) {
// Appelé chaque fois que setState est appelé
print('build appelé');
return const Text('Widget');
}
}
Gestion d'état complexe avec setState
État avec plusieurs variables
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Form Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Formulaire avec plusieurs variables'),
),
body: const Padding(
padding: EdgeInsets.all(16.0),
child: FormWidget(),
),
),
);
}
}
class FormWidget extends StatefulWidget {
const FormWidget({super.key});
State<FormWidget> createState() => _FormWidgetState();
}
class _FormWidgetState extends State<FormWidget> {
String _name = '';
int _age = 0;
bool _isStudent = false;
void _updateName(String value) {
setState(() {
_name = value;
});
}
void _updateAge(int value) {
setState(() {
_age = value;
});
}
void _toggleStudent() {
setState(() {
_isStudent = !_isStudent;
});
}
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: _updateName,
decoration: const InputDecoration(labelText: 'Nom'),
),
Slider(
value: _age.toDouble(),
min: 0,
max: 100,
onChanged: (value) => _updateAge(value.toInt()),
),
CheckboxListTile(
title: const Text('Étudiant'),
value: _isStudent,
onChanged: (_) => _toggleStudent(),
),
Text('$_name, $_age ans, ${_isStudent ? "étudiant" : "non étudiant"}'),
],
);
}
}
État avec classe de données
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'User Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text('État avec classe de données'),
),
body: const Padding(
padding: EdgeInsets.all(16.0),
child: UserWidget(),
),
),
);
}
}
class User {
final String name;
final String email;
final int age;
User({required this.name, required this.email, required this.age});
User copyWith({String? name, String? email, int? age}) {
return User(
name: name ?? this.name,
email: email ?? this.email,
age: age ?? this.age,
);
}
}
class UserWidget extends StatefulWidget {
const UserWidget({super.key});
State<UserWidget> createState() => _UserWidgetState();
}
class _UserWidgetState extends State<UserWidget> {
User _user = User(name: 'John', email: 'john@example.com', age: 25);
void _updateName(String name) {
setState(() {
_user = _user.copyWith(name: name);
});
}
void _incrementAge() {
setState(() {
_user = _user.copyWith(age: _user.age + 1);
});
}
Widget build(BuildContext context) {
return Column(
children: [
Text('${_user.name}, ${_user.age} ans'),
TextField(
onChanged: _updateName,
decoration: const InputDecoration(labelText: 'Nom'),
),
ElevatedButton(
onPressed: _incrementAge,
child: const Text('Augmenter l\'âge'),
),
],
);
}
}
Limites de setState
Quand setState devient insuffisant
- État partagé entre widgets distants : Difficile de faire remonter l'état
- Logique métier complexe : Mélange UI et logique
- Multiples sources de vérité : Duplication d'état
- Performance : Reconstructions inutiles
// PROBLÈME : État partagé compliqué
class ParentWidget extends StatefulWidget {
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
int _counter = 0;
void _increment() {
setState(() => _counter++);
}
Widget build(BuildContext context) {
return Column(
children: [
// Passer la fonction et la valeur à travers plusieurs niveaux
ChildWidget(counter: _counter, onIncrement: _increment),
AnotherChildWidget(counter: _counter),
],
);
}
}
InheritedWidget : Partage d'état basique
InheritedWidget permet de partager des données dans l'arbre de widgets sans passer par les constructeurs.
Exemple simple
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'InheritedWidget Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
useMaterial3: true,
),
home: CounterProvider(
child: Scaffold(
appBar: AppBar(
title: const Text('InheritedWidget Example'),
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DisplayCounter(),
SizedBox(height: 20),
IncrementButton(),
],
),
),
),
),
);
}
}
class CounterInherited extends InheritedWidget {
final int counter;
final VoidCallback increment;
const CounterInherited({
super.key,
required this.counter,
required this.increment,
required super.child,
});
static CounterInherited? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterInherited>();
}
bool updateShouldNotify(CounterInherited oldWidget) {
return counter != oldWidget.counter;
}
}
class CounterProvider extends StatefulWidget {
final Widget child;
const CounterProvider({super.key, required this.child});
State<CounterProvider> createState() => _CounterProviderState();
}
class _CounterProviderState extends State<CounterProvider> {
int _counter = 0;
void _increment() {
setState(() => _counter++);
}
Widget build(BuildContext context) {
return CounterInherited(
counter: _counter,
increment: _increment,
child: widget.child,
);
}
}
// Utilisation
class DisplayCounter extends StatelessWidget {
const DisplayCounter({super.key});
Widget build(BuildContext context) {
final inherited = CounterInherited.of(context)!;
return Text('Counter: ${inherited.counter}');
}
}
class IncrementButton extends StatelessWidget {
const IncrementButton({super.key});
Widget build(BuildContext context) {
final inherited = CounterInherited.of(context)!;
return ElevatedButton(
onPressed: inherited.increment,
child: const Text('Increment'),
);
}
}
Optimisation avec setState
Minimiser les reconstructions
// MAUVAIS : Reconstruit tout
class BadExample extends StatefulWidget {
State<BadExample> createState() => _BadExampleState();
}
class _BadExampleState extends State<BadExample> {
int _counter = 0;
Widget build(BuildContext context) {
return Column(
children: [
ExpensiveWidget(), // Reconstruit à chaque setState
Text('$_counter'),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: const Text('Increment'),
),
],
);
}
}
// BON : Widget séparé
class GoodExample extends StatelessWidget {
const GoodExample({super.key});
Widget build(BuildContext context) {
return Column(
children: [
const ExpensiveWidget(), // const = pas reconstruit
const CounterDisplay(),
],
);
}
}
class CounterDisplay extends StatefulWidget {
const CounterDisplay({super.key});
State<CounterDisplay> createState() => _CounterDisplayState();
}
class _CounterDisplayState extends State<CounterDisplay> {
int _counter = 0;
void _increment() {
setState(() => _counter++);
}
Widget build(BuildContext context) {
return Column(
children: [
Text('$_counter'),
ElevatedButton(
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}
Bonnes pratiques
- setState minimal : Ne modifier que ce qui doit changer
- const : Utiliser const pour widgets statiques
- Séparer les widgets : Limiter les reconstructions
- copyWith : Pour objets immuables
- Cycle de vie : Libérer les ressources dans dispose
- InheritedWidget : Pour état partagé simple
- Solutions avancées : Provider/Riverpod/BLoC pour état complexe