What is Provider and why is it useful?

High PriorityAsked in ~70% of Flutter interviews

3 min read

State Management

Theory — the moving parts

PieceRole
ChangeNotifierHolds your mutable state; calls notifyListeners() to publish changes
ChangeNotifierProviderInserts 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

ProviderHoldsUse for
Provider<T>A plain value or serviceDI for ApiClient, Logger, Repository
ChangeNotifierProvider<T>A ChangeNotifierMutable, observable state
FutureProvider<T>Result of a FutureLoaded-once async data
StreamProvider<T>Latest value of a StreamReal-time data
ProxyProvider<A, R>A value derived from another providerCompose / inject dependencies
MultiProviderMany providers at onceTidy 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

  1. What's the difference between watch, read, and select? watch subscribes the current build context to the provider — rebuilds on every notifyListeners. read does a one-shot read with no subscription — safe in callbacks/initState. select subscribes only to a specific slice — rebuilds only when that slice changes, which is the cheapest of the three.

  2. What does Provider give you that raw InheritedWidget doesn't? Ergonomic context.watch/read/select, auto-disposal of ChangeNotifiers, MultiProvider, async providers (Future/Stream), and ProxyProvider for derived state. The underlying mechanism is the same.

  3. What happens if you forget notifyListeners? The state changes in memory, but no widget rebuilds — your UI looks "frozen." Listeners only run when notifyListeners is called, so every public mutator must call it (or use a wrapper that does).

  4. 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.