Provider In-Depth

High PriorityAsked in ~70% of mid-level interviews

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.

StepMechanism
ProvideChangeNotifierProvider(create: ...) builds an InheritedProvider that holds the notifier and listens for notifyListeners()
Subscribecontext.watch<T>() calls dependOnInheritedWidgetOfExactType — registering the calling element as a dependent
NotifyWhen the notifier fires, the InheritedProvider rebuilds and updateShouldNotify returns true — Flutter marks all dependents dirty
Selectcontext.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

GoalUse
Rebuild the whole widget on any notifycontext.watch<T>()
Rebuild only when a slice changescontext.select<T, R>((t) => t.field)
Read once, no rebuild (callbacks, initState)context.read<T>()
Scope rebuild to a small subtreeConsumer<T>(builder: ...)
React to a value without rebuilding (analytics, navigation)context.select + a useEffect pattern, or a Selector
Multiple providers at the rootMultiProvider(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

  1. How would you test a widget that uses Provider? Wrap it in a ChangeNotifierProvider.value(value: mockNotifier, child: ...) inside pumpWidget. You can stub notifyListeners calls and assert on rebuilds. Or use ChangeNotifierProvider(create: (_) => MockNotifier()) if you want auto-disposal.

  2. What's the difference between Consumer<T> and context.watch<T>()? They do the same thing, with one difference: Consumer scopes the rebuild to its builder. So if only part of a screen needs to rebuild on changes, wrapping it in Consumer is cheaper than watching at the top.

  3. What's the difference between Provider.value and Provider(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 .value for testing and for objects whose lifetime you control elsewhere.

  4. How does context.select decide 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.