Kodokon kodokon.com

Gestion d'état structurée : ChangeNotifier et Provider

Partagez l'état applicatif avec ChangeNotifier et Provider en maîtrisant watch, read et select pour des rebuilds ciblés.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

setState suffit tant que l'état vit et meurt avec un seul écran. Dès qu'une donnée est partagée - panier, session, préférences - la faire descendre de constructeur en constructeur devient ingérable. ChangeNotifier fournit l'observable minimal du SDK ; provider l'injecte dans l'arbre et reconstruit uniquement les widgets abonnés. Face à Riverpod ou Bloc, ce duo reste le plus simple à auditer : peu de magie, une seule dépendance, une mécanique que vous pouvez expliquer en une phrase.

BASH
flutter pub add provider

Un notifier bien conçu encapsule : champs privés, getters en lecture seule, mutations uniquement via des méthodes qui se terminent par notifyListeners(). C'est ce contrat qui garantit qu'aucun changement d'état n'échappe à l'interface.

DART
import 'package:flutter/foundation.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);

  int get count => _items.length;

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }

  void remove(String item) {
    _items.remove(item);
    notifyListeners();
  }
}
État privé, lecture seule vers l'extérieur, notification.

Trois portes d'entrée, trois usages. context.watch<T>() abonne le widget : il se reconstruit à chaque notifyListeners. context.read<T>() lit sans abonner : c'est l'outil des callbacks comme onPressed. context.select<T, R>() n'abonne qu'à une projection : le widget ne se reconstruit que si cette valeur précise change. Règle d'or : watch et select dans build uniquement, read dans les handlers - jamais l'inverse.

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

class CounterModel extends ChangeNotifier {
  int _value = 0;

  int get value => _value;

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

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

class CounterApp extends StatelessWidget {
  const CounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    final value = context.watch<CounterModel>().value;
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Count: $value')),
        floatingActionButton: FloatingActionButton(
          onPressed: () =>
              context.read<CounterModel>().increment(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
watch dans build pour lire, read dans le callback pour agir.
DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  int get count => _items.length;
}

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select(
      (CartModel cart) => cart.count,
    );
    return Badge(
      label: Text('$count'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}
CartBadge ne se reconstruit que si count change.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Dans un onPressed, comment déclencher cart.add(...) sans créer d'abonnement inutile ?
    • context.watch<CartModel>().add(...)
    • context.read<CartModel>().add(...)
    • context.select((CartModel c) => c.add(...))
  2. Que déclenche exactement notifyListeners() ?
    • La reconstruction des seuls widgets qui utilisent la donnée modifiée.
    • Une reconstruction complète de l'application.
    • La reconstruction de tous les widgets abonnés au notifier, quelle que soit la donnée modifiée.
  3. Pourquoi exposer List.unmodifiable(_items) plutôt que _items directement ?
    • Pour améliorer les performances de lecture de la liste.
    • Pour empêcher une mutation externe qui changerait l'état sans notifyListeners, donc sans rebuild.
    • Parce que Provider refuse d'injecter des listes mutables.