Provider In-Depth
4 min read
State Management
Provider is InheritedWidget + ChangeNotifier + ergonomic helpers: ChangeNotifierProvider inserts an InheritedProvider into the tree and rebuilds dependents via the same dependency-registration mechanism Flutter uses for Theme.of. Master watch / read / select and you control rebuild scope precisely.
| Step | Mechanism |
|---|---|
| Provide | ChangeNotifierProvider(create: ...) builds an InheritedProvider that holds the notifier and listens for notifyListeners() |
| Subscribe | context.watch<T>() calls dependOnInheritedWidgetOfExactType — registering the calling element as a dependent |
| Notify | When the notifier fires, the InheritedProvider rebuilds and updateShouldNotify returns true — Flutter marks all dependents dirty |
| Select | context.select((T t) => t.field) subscribes only to changes in that derived value (compared with ==) |
The whole "magic" is the standard InheritedWidget dependency graph — Provider just adds ergonomic helpers and lifecycle management.
Code in action — watch vs read vs select
class CounterNotifier extends ChangeNotifier {
int _count = 0;
String _label = 'Counter';
int get count => _count;
String get label => _label;
void increment() { _count++; notifyListeners(); }
void setLabel(String l){ _label = l; notifyListeners(); }
}
// ❌ Rebuilds on EVERY notify — even when only the label changed
class CountText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = context.watch<CounterNotifier>();
return Text('${c.count}');
}
}
// ✅ Subscribes only to `count`
class CountTextOk extends StatelessWidget {
@override
Widget build(BuildContext context) {
final count = context.select<CounterNotifier, int>((c) => c.count);
return Text('$count');
}
}
// ✅ In callbacks, use `read` — no subscription, no needless rebuilds
ElevatedButton(
onPressed: () => context.read<CounterNotifier>().increment(),
child: const Text('+'),
);
Decision table — rebuild scope
| Goal | Use |
|---|---|
| Rebuild the whole widget on any notify | context.watch<T>() |
| Rebuild only when a slice changes | context.select<T, R>((t) => t.field) |
| Read once, no rebuild (callbacks, initState) | context.read<T>() |
| Scope rebuild to a small subtree | Consumer<T>(builder: ...) |
| React to a value without rebuilding (analytics, navigation) | context.select + a useEffect pattern, or a Selector |
| Multiple providers at the root | MultiProvider(providers: [...]) |
Common mistakes to avoid
// ❌ watch in a callback → throws "watch can't be used in callbacks"
onPressed: () => context.watch<CartNotifier>().add(item);
// ✅ context.read<CartNotifier>().add(item);
// ❌ read in build() when you actually want to rebuild
@override
Widget build(BuildContext context) {
final cart = context.read<CartNotifier>(); // never rebuilds!
return Text('${cart.itemCount}');
}
// ✅ watch or select
// ❌ Forgetting notifyListeners after mutation
void add(Item i) { items.add(i); } // UI never updates
// ✅ items.add(i); notifyListeners();
// ❌ Mutating the same list reference and using select on identity
final items = <Item>[];
items.add(...); // same list — select may skip
// ✅ Emit a NEW list: items = [...items, i];
// ❌ Watching a large notifier from a high-up widget
// The entire screen rebuilds on any notify. Push watch lower or use select.
Interview follow-ups
-
How would you test a widget that uses Provider? Wrap it in a
ChangeNotifierProvider.value(value: mockNotifier, child: ...)insidepumpWidget. You can stubnotifyListenerscalls and assert on rebuilds. Or useChangeNotifierProvider(create: (_) => MockNotifier())if you want auto-disposal. -
What's the difference between
Consumer<T>andcontext.watch<T>()? They do the same thing, with one difference:Consumerscopes the rebuild to itsbuilder. So if only part of a screen needs to rebuild on changes, wrapping it inConsumeris cheaper thanwatching at the top. -
What's the difference between
Provider.valueandProvider(create: ...)?create:builds the value lazily and disposes it when removed..value:takes an existing instance and doesn't dispose it — you own its lifetime. Use.valuefor testing and for objects whose lifetime you control elsewhere. -
How does
context.selectdecide whether to rebuild? It runs the selector each time the provider notifies, compares the new derived value with the previous one using==, and rebuilds only if it changed. That's why returning a new collection identity on every notify defeats it — you need value equality (records, equatable, or a primitive).
How helpful was this content?
Please sign in to rate this article.