What is Provider and why is it useful?
3 min read
State Management
Theory — the moving parts
| Piece | Role |
|---|---|
ChangeNotifier | Holds your mutable state; calls notifyListeners() to publish changes |
ChangeNotifierProvider | Inserts the notifier into the widget tree, disposes it for you |
context.watch<T>() | Reads + subscribes — rebuilds the calling widget when T notifies |
context.read<T>() | Reads once, no subscription — safe in callbacks |
context.select<T, R>((t) => t.field) | Subscribes only to changes in field — minimises rebuilds |
Consumer<T> | Builder that scopes the rebuild to a subtree |
Provider isn't magic — it's InheritedWidget + ChangeNotifier + nice ergonomics.
Code in action — counter end-to-end
// 1. The state
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // tells dependents to rebuild
}
}
// 2. Provide it high in the tree
void main() => runApp(
ChangeNotifierProvider(
create: (_) => CounterNotifier(),
child: const MyApp(),
),
);
// 3. Consume
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterNotifier>(); // subscribe
return Column(children: [
Text('${counter.count}'),
ElevatedButton(
onPressed: () => context.read<CounterNotifier>().increment(), // no subscribe
child: const Text('+'),
),
]);
}
}
Provider flavours — pick the right one
| Provider | Holds | Use for |
|---|---|---|
Provider<T> | A plain value or service | DI for ApiClient, Logger, Repository |
ChangeNotifierProvider<T> | A ChangeNotifier | Mutable, observable state |
FutureProvider<T> | Result of a Future | Loaded-once async data |
StreamProvider<T> | Latest value of a Stream | Real-time data |
ProxyProvider<A, R> | A value derived from another provider | Compose / inject dependencies |
MultiProvider | Many providers at once | Tidy app root |
watch vs read vs select
// watch — subscribe; rebuilds on every notify
final auth = context.watch<AuthNotifier>();
// read — one-shot; no rebuild (use in callbacks / initState)
context.read<AuthNotifier>().logout();
// select — subscribe to ONE slice; rebuild only on that slice changing
final isLoggedIn = context.select<AuthNotifier, bool>((a) => a.isLoggedIn);
Rule of thumb: read in callbacks, watch in build, select for performance.
Common mistakes to avoid
// ❌ Calling watch() in a callback / initState
onPressed: () => context.watch<CartNotifier>().add(item); // throws
// ✅ Use context.read in callbacks
// ❌ Forgetting notifyListeners()
class Cart extends ChangeNotifier {
final items = <Item>[];
void add(Item i) { items.add(i); } // UI never updates
}
// ✅ items.add(i); notifyListeners();
// ❌ Mutating the same object instance and expecting UI to refresh
items.add(i); notifyListeners();
context.select((c) => c.items) // identical list → may skip rebuild
// ✅ Emit a new list (`items = [...items, i]`) or use a fresh value-equal model
// ❌ Rebuilding the world by watching too high in the tree
final cart = context.watch<CartNotifier>(); // whole screen rebuilds
// ✅ Use a narrower Consumer or context.select
// ❌ Creating the notifier with `value:` and disposing manually somewhere else
// ChangeNotifierProvider(create: ...) auto-disposes. With .value, YOU manage lifetime.
Interview follow-ups
-
What's the difference between
watch,read, andselect?watchsubscribes the current build context to the provider — rebuilds on everynotifyListeners.readdoes a one-shot read with no subscription — safe in callbacks/initState.selectsubscribes only to a specific slice — rebuilds only when that slice changes, which is the cheapest of the three. -
What does Provider give you that raw InheritedWidget doesn't? Ergonomic
context.watch/read/select, auto-disposal ofChangeNotifiers,MultiProvider, async providers (Future/Stream), andProxyProviderfor derived state. The underlying mechanism is the same. -
What happens if you forget
notifyListeners? The state changes in memory, but no widget rebuilds — your UI looks "frozen." Listeners only run whennotifyListenersis called, so every public mutator must call it (or use a wrapper that does). -
When would you choose Provider over Riverpod/BLoC? For small/medium apps where simplicity matters, where you're already familiar with InheritedWidget, or where the team wants minimal mental overhead. Riverpod offers compile-time safety and no context dependency; BLoC offers a stricter event-driven discipline. Provider sits in the middle.
How helpful was this content?
Please sign in to rate this article.