Aller au contenu principal

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

  1. Toujours appeler setState pour modifier l'état
  2. Modifications dans setState : placer les changements d'état à l'intérieur
  3. 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

  1. État partagé entre widgets distants : Difficile de faire remonter l'état
  2. Logique métier complexe : Mélange UI et logique
  3. Multiples sources de vérité : Duplication d'état
  4. 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

  1. setState minimal : Ne modifier que ce qui doit changer
  2. const : Utiliser const pour widgets statiques
  3. Séparer les widgets : Limiter les reconstructions
  4. copyWith : Pour objets immuables
  5. Cycle de vie : Libérer les ressources dans dispose
  6. InheritedWidget : Pour état partagé simple
  7. Solutions avancées : Provider/Riverpod/BLoC pour état complexe