ValueNotifier et ChangeNotifier
Le pattern Flutter : Notifiers
Section titled “Le pattern Flutter : Notifiers”Flutter propose deux classes intégrées implémentant 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
Ce pattern permet de séparer la logique métier de l’interface utilisateur et d’améliorer la réactivité de votre application.
ValueNotifier
Section titled “ValueNotifier”Concept de base
Section titled “Concept de base”ValueNotifier permet de définir un objet observable que des widgets pourront “surveiller” et être prévenus des modifications automatiquement.
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 pour encapsuler la logique métier :
class CounterController extends ValueNotifier<int> { CounterController(int value) : super(value);
void increment() => value++; void decrement() => value--;
void reset() => value = 0;
bool get isPositive => value > 0; bool get isEven => value % 2 == 0;}Utilisation avec des types complexes
Section titled “Utilisation avec des types complexes”class UserController extends ValueNotifier<User?> { UserController() : super(null);
void login(String email, String password) async { // Logique d'authentification final user = await AuthService.login(email, password); value = user; }
void logout() { value = null; }
bool get isLoggedIn => value != null;}ChangeNotifier
Section titled “ChangeNotifier”Concept avancé
Section titled “Concept avancé”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(); }}ChangeNotifier pour gestion d’état complexe
Section titled “ChangeNotifier pour gestion d’état complexe”class ShoppingCartController extends ChangeNotifier { final List<CartItem> _items = [];
List<CartItem> get items => List.unmodifiable(_items);
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price * item.quantity);
int get itemCount => _items.fold(0, (sum, item) => sum + item.quantity);
void addItem(Product product) { final existingIndex = _items.indexWhere((item) => item.product.id == product.id);
if (existingIndex >= 0) { _items[existingIndex].quantity++; } else { _items.add(CartItem(product: product, quantity: 1)); }
notifyListeners(); }
void removeItem(String productId) { _items.removeWhere((item) => item.product.id == productId); notifyListeners(); }
void updateQuantity(String productId, int quantity) { final index = _items.indexWhere((item) => item.product.id == productId); if (index >= 0) { if (quantity <= 0) { _items.removeAt(index); } else { _items[index].quantity = quantity; } notifyListeners(); } }
void clear() { _items.clear(); notifyListeners(); }}Observer Pattern en pratique
Section titled “Observer Pattern en pratique”Écouter les changements
Section titled “Écouter les changements”Avec ValueNotifier :
final counter = CounterController(0);
// Écouter les changementscounter.addListener(() { print('Counter changed: ${counter.value}');});
// Modifier la valeurcounter.increment(); // Déclenche l'écouteAvec ChangeNotifier :
final cart = ShoppingCartController();
// Écouter les changementscart.addListener(() { print('Cart updated: ${cart.itemCount} items, total: \$${cart.totalPrice}');});
// Ajouter un produitcart.addItem(product); // Déclenche l'écouteUtilisation dans les widgets
Section titled “Utilisation dans les widgets”class CartWidget extends StatefulWidget { @override _CartWidgetState createState() => _CartWidgetState();}
class _CartWidgetState extends State<CartWidget> { late ShoppingCartController _cartController;
@override void initState() { super.initState(); _cartController = ShoppingCartController(); _cartController.addListener(_onCartChanged); }
@override void dispose() { _cartController.removeListener(_onCartChanged); _cartController.dispose(); super.dispose(); }
void _onCartChanged() { setState(() { // Le widget sera reconstruit }); }
@override Widget build(BuildContext context) { return Column( children: [ Text('Items: ${_cartController.itemCount}'), Text('Total: \$${_cartController.totalPrice}'), // ... ], ); }}Bonnes pratiques avec les Notifiers
Section titled “Bonnes pratiques avec les Notifiers”1. Gestion de la mémoire
Section titled “1. Gestion de la mémoire”class MyWidget extends StatefulWidget { @override _MyWidgetState createState() => _MyWidgetState();}
class _MyWidgetState extends State<MyWidget> { late ValueNotifier<int> _counter;
@override void initState() { super.initState(); _counter = ValueNotifier(0); }
@override void dispose() { _counter.dispose(); // Important : libérer les ressources super.dispose(); }
@override Widget build(BuildContext context) { return ValueListenableBuilder<int>( valueListenable: _counter, builder: (context, value, child) { return Text('$value'); }, ); }}2. Séparation des responsabilités
Section titled “2. Séparation des responsabilités”// ❌ Mauvais : logique mélangéeclass BadCounterController extends ValueNotifier<int> { BadCounterController() : super(0);
void increment() { value++; // Logique d'interface mélangée print('Nouveau compteur: $value'); if (value > 10) { showDialog(/* ... */); // ❌ Ne devrait pas être ici } }}
// ✅ Bon : logique séparéeclass GoodCounterController extends ValueNotifier<int> { GoodCounterController() : super(0);
void increment() { value++; // Seule la logique métier }
bool get shouldShowWarning => value > 10;}3. Tests unitaires
Section titled “3. Tests unitaires”void main() { group('CounterController', () { test('should increment value', () { final controller = CounterController(0);
controller.increment();
expect(controller.value, equals(1)); });
test('should notify listeners on increment', () { final controller = CounterController(0); bool wasNotified = false;
controller.addListener(() { wasNotified = true; });
controller.increment();
expect(wasNotified, isTrue); }); });}Conclusion
Section titled “Conclusion”Les ValueNotifier et ChangeNotifier offrent une solution élégante pour :
- Séparer la logique métier de l’interface utilisateur
- Améliorer la testabilité du code
- Implémenter le pattern Observer de manière native
- Gérer l’état réactif sans complexité excessive
Ces notifiers constituent une excellente base avant d’adopter des solutions plus avancées comme Provider ou Bloc, et restent parfaitement adaptés pour de nombreuses applications Flutter.