Understanding Flutter's ValueNotifier and ChangeNotifier
Understanding Flutter’s ValueNotifier and ChangeNotifier
What You’ll Learn
Flutter’s built-in ValueNotifier and ChangeNotifier provide simple, lightweight state management without external packages. They’re perfect for managing local state and building reactive UIs that update automatically when data changes.
Why Use Notifiers?
When you need to share state between widgets or trigger rebuilds when data changes, you have three basic options: setState(), InheritedWidget, or notifiers. Notifiers sit in the sweet spot—more powerful than setState() but simpler than full state management solutions like Provider or Riverpod.
ValueNotifier: Single Value Reactivity
ValueNotifier holds a single value and notifies listeners when it changes.
import 'package:flutter/material.dart';
class CounterExample extends StatefulWidget {
@override
_CounterExampleState createState() => _CounterExampleState();
}
class _CounterExampleState extends State<CounterExample> {
// Create a ValueNotifier to hold counter value
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
@override
void dispose() {
_counter.dispose(); // Always dispose notifiers!
super.dispose();
}
void _increment() {
_counter.value++; // Automatically notifies listeners
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ValueNotifier Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You pressed the button:'),
// ValueListenableBuilder rebuilds only when value changes
ValueListenableBuilder<int>(
valueListenable: _counter,
builder: (context, value, child) {
return Text(
'$value',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Key advantage: Only the ValueListenableBuilder widget rebuilds when the value changes—not the entire page. This is more efficient than setState() which rebuilds the whole widget subtree.
ChangeNotifier: Complex State Management
For managing multiple values or complex state, use ChangeNotifier:
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalPrice => _items.length * 9.99;
void addItem(String item) {
_items.add(item);
notifyListeners(); // Trigger rebuild of all listeners
}
void removeItem(String item) {
_items.remove(item);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
}
class CartScreen extends StatefulWidget {
@override
_CartScreenState createState() => _CartScreenState();
}
class _CartScreenState extends State<CartScreen> {
final CartModel _cart = CartModel();
@override
void dispose() {
_cart.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Shopping Cart')),
body: Column(
children: [
// Listen to cart changes
ListenableBuilder(
listenable: _cart,
builder: (context, child) {
return Padding(
padding: EdgeInsets.all(16),
child: Text(
'Total: \$${_cart.totalPrice.toStringAsFixed(2)} (${_cart.itemCount} items)',
style: Theme.of(context).textTheme.titleLarge,
),
);
},
),
Expanded(
child: ListenableBuilder(
listenable: _cart,
builder: (context, child) {
if (_cart.items.isEmpty) {
return Center(child: Text('Cart is empty'));
}
return ListView.builder(
itemCount: _cart.itemCount,
itemBuilder: (context, index) {
final item = _cart.items[index];
return ListTile(
title: Text(item),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _cart.removeItem(item),
),
);
},
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _cart.addItem('Item ${_cart.itemCount + 1}'),
child: Icon(Icons.add_shopping_cart),
),
);
}
}
When to Use Each
ValueNotifier:
- Single primitive value (int, bool, String)
- Simple toggles or counters
- No complex logic needed
ChangeNotifier:
- Multiple related values
- Complex state with business logic
- Need getters/computed properties
- Building a mini state management solution
Try It Yourself
Create a theme switcher using ValueNotifier<bool>:
- Create a
ValueNotifier<bool>for dark mode on/off - Use
ValueListenableBuilderto rebuildMaterialAppwith different themes - Add a toggle button that changes the notifier value
- Notice how only the necessary parts of the UI rebuild
Bonus challenge: Extend it to support three themes (light, dark, system) using ValueNotifier<ThemeMode>.
Tip of the Day
Performance tip: When using ChangeNotifier, avoid calling notifyListeners() too frequently. If you’re updating multiple properties, batch them together and call notifyListeners() once at the end. Also, consider using ValueNotifier for individual values that change independently—this gives you more granular control over what rebuilds when.
// Bad: Multiple notifications
void badUpdate() {
_count++;
notifyListeners();
_name = 'New Name';
notifyListeners();
}
// Good: Single notification
void goodUpdate() {
_count++;
_name = 'New Name';
notifyListeners(); // Only notify once
}