Flutter Lesson 39 of 83 4 min read
ValueNotifier and ChangeNotifier in Flutter
Manage state in Flutter without packages using ValueNotifier, ChangeNotifier, ValueListenableBuilder and ListenableBuilder.
On this page
Flutter includes two small classes for state that lives outside a widget. They are the foundation of Provider, and for small apps they may be all you need.
Both are listenables: objects that widgets can subscribe to, and that announce when they have changed.
ValueNotifier: one value #
import 'package:flutter/material.dart';
final counter = ValueNotifier<int>(0);
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) {
return Text('$value', style: const TextStyle(fontSize: 48));
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.value++,
child: const Icon(Icons.add),
),
),
),
);
}
There is no StatefulWidget and no setState. Assigning to counter.value notifies listeners, and ValueListenableBuilder rebuilds only its own builder, not the whole page.
A ValueNotifier notifies only when the new value is not equal to the old one. That has a consequence for collections:
final items = ValueNotifier<List<String>>([]);
items.value.add('pen'); // no rebuild: it is still the same list
items.value = [...items.value, 'pen']; // rebuilds: a new list
ChangeNotifier: a model with several values #
For state with more than one field, and methods that change it, extend ChangeNotifier and call notifyListeners() after each change.
import 'package:flutter/material.dart';
class CartModel extends ChangeNotifier {
final Map<String, int> _prices = {};
List<String> get items => _prices.keys.toList();
int get count => _prices.length;
int get total => _prices.values.fold(0, (sum, p) => sum + p);
void add(String name, int price) {
_prices[name] = price;
notifyListeners();
}
void remove(String name) {
_prices.remove(name);
notifyListeners();
}
}
final cart = CartModel();
void main() => runApp(const MaterialApp(home: CartPage()));
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cart')),
body: ListenableBuilder(
listenable: cart,
builder: (context, child) {
if (cart.count == 0) return const Center(child: Text('Your cart is empty'));
return ListView(
children: [
for (final item in cart.items)
ListTile(
title: Text(item),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => cart.remove(item),
),
),
ListTile(title: Text('Total: Rs. ${cart.total}')),
],
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => cart.add('Item ${cart.count + 1}', 100 * (cart.count + 1)),
child: const Icon(Icons.add),
),
);
}
}
The model keeps its data private and exposes getters and methods. The UI cannot change the cart except through add and remove, so the rules stay in one place and the class can be unit tested with no widgets.
The builders #
| Builder | Listens to | Builder receives |
|---|---|---|
ValueListenableBuilder<T> | A ValueNotifier<T> | (context, value, child) |
ListenableBuilder | Any Listenable: a ChangeNotifier, an animation, a controller | (context, child) |
Both take an optional child: a part of the subtree that does not depend on the value. It is built once and handed back to the builder, saving work.
ValueListenableBuilder<int>(
valueListenable: counter,
child: const ExpensiveHeader(), // built once
builder: (context, value, child) => Column(
children: [child!, Text('$value')],
),
)
Wrap the smallest widget that needs the value. A builder around the whole page rebuilds the whole page.
Listening without building #
To run code on a change, such as showing a snackbar, add a listener, and always remove it.
@override
void initState() {
super.initState();
cart.addListener(_onCartChanged);
}
void _onCartChanged() {
if (cart.count == 10) debugPrint('Cart is getting full');
}
@override
void dispose() {
cart.removeListener(_onCartChanged);
super.dispose();
}
Disposing #
A notifier created by a widget must be disposed by that widget.
class _SearchPageState extends State<SearchPage> {
final _query = ValueNotifier('');
@override
void dispose() {
_query.dispose();
super.dispose();
}
}
Where does the model live? #
The examples use a global variable to stay short. Globals work, but they are awkward to replace in tests and cannot be scoped to part of the app. The usual answer is to place the model in the tree, with an InheritedNotifier or, far more commonly, with the provider package in the next lesson.
When this is enough #
ValueNotifier and ChangeNotifier with the built-in builders are a complete, dependency-free solution. Many production apps use exactly this. Reach for a package when you want easier access from deep in the tree, scoping, or built-in handling of loading and error states.
Try it yourself #
Write a TodoModel extending ChangeNotifier, with add, toggle and remove. Show the list with a ListenableBuilder, and show a “3 of 5 done” summary in the app bar with its own separate builder.