State Management with ValueNotifier and ChangeNotifier
State Management with ValueNotifier and ChangeNotifier
What You’ll Learn
Discover Flutter’s built-in lightweight state management using ValueNotifier and ChangeNotifier. These powerful tools let you manage app state efficiently without external packages, perfect for small to medium-sized apps.
Why ValueNotifier and ChangeNotifier?
Before diving into complex state management solutions like Provider or Riverpod, master these built-in Flutter tools. They’re the foundation many packages are built on.
When to use:
- Managing simple reactive state (counters, toggles, form state)
- Building custom controllers for widgets
- Learning state management fundamentals
- Avoiding setState() spaghetti code
Benefits:
- Zero dependencies (built into Flutter)
- Minimal boilerplate
- Easy to understand and debug
- Foundation for understanding Provider and other solutions
ValueNotifier: For Simple Values
ValueNotifier wraps a single value and notifies listeners when it changes. Think of it as a reactive variable.
Basic Example:
class CounterScreen extends StatefulWidget {
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
// Create a ValueNotifier with initial value
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
@override
void dispose() {
_counter.dispose(); // Always dispose to prevent memory leaks
super.dispose();
}
void _increment() {
_counter.value++; // Updates value and notifies listeners automatically
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ValueNotifier Counter')),
body: Center(
// ValueListenableBuilder rebuilds only when value changes
child: ValueListenableBuilder<int>(
valueListenable: _counter,
builder: (context, count, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: $count',
style: TextStyle(fontSize: 48),
),
// This child widget is NOT rebuilt (performance optimization)
if (child != null) child,
],
);
},
// Static child passed to builder (won't rebuild)
child: Padding(
padding: EdgeInsets.all(16),
child: Text('This text never rebuilds!'),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Key points:
- Only the
ValueListenableBuilderrebuilds, not the entire screen - The optional
childparameter avoids rebuilding static widgets - Much more efficient than
setState()for localized updates
ChangeNotifier: For Complex State
When you need to manage multiple values or complex state, use ChangeNotifier. It’s like ValueNotifier but handles multiple properties.
Example: Shopping Cart
// 1. Create a model that extends ChangeNotifier
class ShoppingCart extends ChangeNotifier {
final List<CartItem> _items = [];
// Getters
List<CartItem> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalPrice {
return _items.fold(0, (sum, item) => sum + item.price);
}
// Methods that modify state
void addItem(CartItem item) {
_items.add(item);
notifyListeners(); // Notify all listeners about the change
}
void removeItem(String itemId) {
_items.removeWhere((item) => item.id == itemId);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
bool contains(String itemId) {
return _items.any((item) => item.id == itemId);
}
}
class CartItem {
final String id;
final String name;
final double price;
CartItem({required this.id, required this.name, required this.price});
}
// 2. Use it in your widget
class ShoppingCartScreen extends StatefulWidget {
@override
State<ShoppingCartScreen> createState() => _ShoppingCartScreenState();
}
class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
final ShoppingCart _cart = ShoppingCart();
@override
void dispose() {
_cart.dispose(); // ChangeNotifier needs disposal
super.dispose();
}
void _addSampleItem() {
final item = CartItem(
id: DateTime.now().toString(),
name: 'Product ${_cart.itemCount + 1}',
price: 10.0 + (_cart.itemCount * 5),
);
_cart.addItem(item);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Shopping Cart'),
actions: [
// Listen to cart changes
AnimatedBuilder(
animation: _cart,
builder: (context, child) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'\$${_cart.totalPrice.toStringAsFixed(2)}',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
);
},
),
],
),
body: AnimatedBuilder(
animation: _cart,
builder: (context, child) {
if (_cart.items.isEmpty) {
return Center(child: Text('Cart is empty'));
}
return ListView.builder(
itemCount: _cart.items.length,
itemBuilder: (context, index) {
final item = _cart.items[index];
return ListTile(
title: Text(item.name),
subtitle: Text('\$${item.price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => _cart.removeItem(item.id),
),
);
},
);
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'add',
onPressed: _addSampleItem,
child: Icon(Icons.add_shopping_cart),
),
SizedBox(height: 8),
FloatingActionButton(
heroTag: 'clear',
onPressed: _cart.clear,
child: Icon(Icons.clear),
),
],
),
);
}
}
ValueNotifier vs ChangeNotifier
| Feature | ValueNotifier | ChangeNotifier |
|---|---|---|
| Best for | Single value | Multiple values/complex state |
| Widget | ValueListenableBuilder | AnimatedBuilder or ListenableBuilder |
| Boilerplate | Minimal | Slightly more |
| Use case | Toggles, counters, selections | Models, controllers, services |
Combining Multiple Notifiers
Need to listen to multiple ValueNotifiers? Use Listenable.merge:
final name = ValueNotifier<String>('');
final age = ValueNotifier<int>(0);
// Listen to both
ValueListenableBuilder(
valueListenable: Listenable.merge([name, age]),
builder: (context, child) {
return Text('${name.value}, Age: ${age.value}');
},
)
Try It Yourself
Build a theme switcher using ValueNotifier:
- Create a
ValueNotifier<bool>for dark mode state - Use ValueListenableBuilder to rebuild MaterialApp with theme
- Add a toggle switch to change themes
- Persist the preference using SharedPreferences
Challenge: Build a multi-step form wizard with ChangeNotifier that:
- Tracks current step, form data, and validation state
- Allows navigation between steps
- Shows progress indicator
- Validates before moving to next step
Tip of the Day
Performance trick: Use ValueListenableBuilder’s child parameter for widgets that don’t depend on the value. They’ll be passed through without rebuilding, saving precious CPU cycles.
Debugging: Override toString() in your ChangeNotifier classes to see state in Flutter DevTools. Add debugPrint() in notifyListeners() to track when rebuilds happen.
Bridge to Provider: Once comfortable with ChangeNotifier, learning Provider is easy—it’s just ChangeNotifier with dependency injection built in!