Week 7: Provider for App-Wide State

Week 6 built InheritedWidget and ChangeNotifier by hand to see exactly what they do. This week trades that hand-rolled version for Provider — the package that wraps the same mechanics in a much more ergonomic API, and was the de facto standard Flutter state management approach for years.

Phase 4 of 8 Week 7 of 20 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Expose a ChangeNotifier to the widget tree with ChangeNotifierProvider
  • Consume provided state with Consumer, and understand the difference from context.watch/context.read
  • Organize multiple providers with MultiProvider
  • Recognize the specific ergonomic gap that motivates reaching for Riverpod (next week) over Provider

1. Exposing State with ChangeNotifierProvider

terminal
flutter pub add provider
main.dart — exposing CartModel to the whole app
ChangeNotifierProvider(
  create: (context) => CartModel(),
  child: MaterialApp.router(routerConfig: router),
)

This is functionally the same CartModel from Week 6 — Provider isn't a different state model, it's a cleaner way of doing exactly what last week's hand-rolled InheritedWidget did, with the boilerplate (the static .of(context) accessor, updateShouldNotify) handled for you.

2. Consumer vs. context.watch/read

Consumer — rebuilds just its builder on change
Consumer<CartModel>(
  builder: (context, cart, child) => Badge(label: Text('${cart.items.length}')),
)
context.watch — the same thing, less nesting
@override
Widget build(BuildContext context) {
  final cart = context.watch<CartModel>(); // rebuilds this whole build() on change
  return Badge(label: Text('${cart.items.length}'));
}
context.read — reads once, does not subscribe
ElevatedButton(
  onPressed: () => context.read<CartModel>().add('Widget'), // inside a callback — correct
  child: const Text('Add to Cart'),
)

The rule that trips up nearly everyone at first: context.watch inside a callback (like onPressed) throws, because callbacks don't run during build and have no meaningful "rebuild me" semantics — use context.read there instead, reserving watch/Consumer for the actual build method.

3. MultiProvider

A real app has more than one piece of shared state — auth, cart, user preferences — and nesting ChangeNotifierProviders manually gets unwieldy fast. MultiProvider flattens that.

main.dart — several providers, flat instead of nested
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => CartModel()),
    ChangeNotifierProvider(create: (_) => AuthModel()),
    ChangeNotifierProvider(create: (_) => PreferencesModel()),
  ],
  child: MaterialApp.router(routerConfig: router),
)

Each provider is still independent — a widget watching CartModel only rebuilds when CartModel changes, unaffected by AuthModel or PreferencesModel updating. MultiProvider is purely organizational, not a change to how any individual provider behaves.

4. Hands-on Exercise

Hands-on

Rebuild Week 6's cart with Provider, plus a second provider

Swap the hand-rolled InheritedWidget for Provider, and add a second, independent piece of app-wide state.

Requirements:

  1. Replace Week 6's custom InheritedWidget with ChangeNotifierProvider, keeping the same CartModel class largely unchanged.
  2. Use Consumer in at least one place and context.watch in another, for the same kind of read, to see both styles.
  3. Add a second, independent provider — an AuthModel with an isSignedIn flag is a good fit, since it plugs directly into Week 5's redirect logic.
  4. Organize both with MultiProvider and confirm a change to one does not cause widgets watching only the other to rebuild (verify with a print statement or the Flutter DevTools rebuild counter).
  5. Use context.read correctly inside at least one button's onPressed callback, and confirm using context.watch there instead throws the error this week's section described.
Hint

If you're not sure whether an unrelated provider's change is triggering extra rebuilds, the Flutter DevTools "Track widget rebuilds" feature (or a simple print in each widget's build method) will show you directly — don't just assume Provider's scoping is working correctly without actually checking.

5. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Is Provider a different state management model from what Week 6 built by hand, or the same model with a nicer API?

The same underlying model — a ChangeNotifier made reachable from the widget tree via an InheritedWidget-based mechanism. Provider doesn't introduce new state semantics; it removes the boilerplate (writing your own static accessor, your own updateShouldNotify) around exactly the pattern built manually last week.

Q2

Why does calling context.watch inside an onPressed callback throw an error?

watch subscribes the current build to future rebuilds when the watched value changes — that only makes sense during a widget's build method, which is the one place "rebuild this" is meaningful. A callback like onPressed runs in response to a user action, entirely outside of any build phase, so there's no build context for watch to meaningfully attach a subscription to.

Q3

Why doesn't a change to AuthModel cause a widget that only watches CartModel to rebuild, even though both are registered under the same MultiProvider?

Each provider tracks its own listeners independently — watching CartModel only subscribes to CartModel's own notifyListeners() calls, not to every provider nested anywhere above it. MultiProvider is purely a way to flatten the nesting syntax; it doesn't merge or couple the providers' change notifications together in any way.

Q4

What is context.read for, and why is it the correct choice inside a button callback where context.watch is not?

context.read retrieves the current value once, without subscribing to future changes — exactly what a one-off action like adding an item to the cart needs, since the callback doesn't need to "stay in sync" with the model, it just needs to call a method on it once, at the moment the button is pressed.