Skip to content

ValueNotifier et ChangeNotifier

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

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

Avec ValueNotifier :

final counter = CounterController(0);
// Écouter les changements
counter.addListener(() {
print('Counter changed: ${counter.value}');
});
// Modifier la valeur
counter.increment(); // Déclenche l'écoute

Avec ChangeNotifier :

final cart = ShoppingCartController();
// Écouter les changements
cart.addListener(() {
print('Cart updated: ${cart.itemCount} items, total: \$${cart.totalPrice}');
});
// Ajouter un produit
cart.addItem(product); // Déclenche l'écoute
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}'),
// ...
],
);
}
}
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');
},
);
}
}
// ❌ Mauvais : logique mélangée
class 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ée
class GoodCounterController extends ValueNotifier<int> {
GoodCounterController() : super(0);
void increment() {
value++;
// Seule la logique métier
}
bool get shouldShowWarning => value > 10;
}
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);
});
});
}

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.