ChangeNotifier is a class that holds state and calls notifyListeners() after each change; ChangeNotifierProvider creates it, puts it into the widget tree and disposes it when it leaves.
Keep fields private: expose getters and methods, so every change goes through a method that notifies
**notifyListeners(): tells every listener, including the provider, that something changed; dependents rebuild in the next frame
In the UI**: context.watch<Counter>() in build to show the value, context.read<Counter>() in onPressed to call a method
FlutterMiddle
State with Provider
Provider package, ChangeNotifier, Consumer, context.watch/read/select
Questions closed
0 / 21
- 0Review
- 2Asked often
Questions
The two calls are swapped: read in build takes the value once without subscribing, and watch in onPressed is a listen call outside the build phase, so it asserts before increment() ever runs.
The assertion: "Tried to listen to a value exposed with provider, from outside of the widget tree"
The fix: context.watch<Counter>() at the top of build, context.read<Counter>().increment() in onPressed
**Why read in a callback**: a tap needs the object, not a subscription; subscribing there would be pointless even if it were allowed
MultiProvider is a flat way to write nested providers (the same tree, just readable). lazy controls when create runs: on the first read (the default) or as soon as the provider is mounted.
Order is tree order: a provider can read only the ones listed above it, so a dependency goes first
**create runs once: for the provider's lifetime, not on every rebuild
lazy: false**: for warm-up work at start-up, or to surface an init error immediately instead of on the first screen that reads it
Provider<T> only holds a value and never listens to it. It rebuilds dependents only when the instance itself is replaced, and changing a field inside the same instance is invisible to it.
**create runs once: the object is cached; nobody is told when its fields change
Fix 1**: make Settings a ChangeNotifier with a setter that calls notifyListeners(), and expose it with ChangeNotifierProvider
Fix 2: keep Settings immutable and replace the whole instance from a parent through Provider.value
Mental model: plain Provider is dependency injection for what does not change: a repository, an HTTP client, config
watch returns the value and rebuilds the whole widget on every notification, read returns it once with no subscription, select rebuilds only when one derived value changes, and Consumer rebuilds only its builder. Rule: read in callbacks, the other three in build.
**context.watch<T>(): simplest; fine for a small widget that uses most of the state
context.read<T>()**: for onPressed, initState callbacks, create; in build it shows a value that never updates
**context.select<T, R>((t) => t.field)**: the widget rebuilds only when field changes, not when any other field does
**Consumer<T>**: narrows the rebuild to part of a big build method; its child parameter is built once and passed back in
**watch outside build**: in a tap handler it asserts; use read there
They differ in what makes dependents rebuild: Provider exposes a value and never listens, ChangeNotifierProvider rebuilds on notifyListeners(), and FutureProvider and StreamProvider expose the latest result of a Future or a Stream.
**Provider<T>: for objects that do not change: a repository, an API client, config
ChangeNotifierProvider<T>: mutable app state; it also disposes the notifier
FutureProvider<T>**: exposes initialData until the future completes, then the result; for something loaded once, such as remote config
**StreamProvider<T>: exposes the latest event; for auth state, a Firestore query, a socket feed
No loading or error state**: you get initialData or a value; an error without catchError is only reported through FlutterError.reportError and the value stays initialData. When the screen needs loading, error and retry, use a notifier instead
They do not re-run: create runs once; to refresh, remount the provider with a new key or move the call into a notifier
create builds the object and makes the provider its owner: built once, disposed when the provider leaves the tree. .value exposes an object that already exists and never disposes it; whoever created it does.
**New object → create**: .value(value: CartNotifier()) inside build makes a new instance on every rebuild and disposes none of them
**Existing object → .value**: re-exposing it in a dialog or a pushed route, an item of a ListView.builder, a fake in a test
**Never create: (_) => existing: the provider would dispose an object it does not own when it unmounts, and the real owner keeps using a dead notifier
Two creates are two objects**: if the products screen and the cart screen each wrap themselves in ChangeNotifierProvider(create: ...), there are two carts and items never show up on the other screen. Provide shared state once, above both
**Plain Provider**: disposes nothing by itself; pass a dispose: callback to close a database or a client
Both subscribe to one value derived from the provider and rebuild only when that value changes, while Consumer and watch rebuild on every notifyListeners(), whatever changed.
**context.select<T, R>(fn)**: inline in build; the whole widget rebuilds, but only when the selected value changes
**Selector<T, R>**: the widget form; adds a child that is never rebuilt and an optional shouldRebuild
How they compare: with DeepCollectionEquality, so a new List or Map with equal elements counts as unchanged; an object of your own class without == always counts as changed
Several fields: select a record (c.count, c.total): records compare by value
Keep the selector cheap: it runs whenever the provider notifies its dependents, so do not sort or filter a big list inside it
**When select is not enough**: most fields change together and the notifier is too coarse; split it
add() mutates the same List in place, and select compares the list it returned last time with the one it returns now. Both are the same object, so they are always equal and CartList never rebuilds.
**How select decides**: it keeps the value from the last build and, on each notification, compares it with the new one using DeepCollectionEquality; a mutated instance is compared with itself
**Why watch hid it**: watch rebuilds on every notifyListeners(), so the bug appears only after someone switches to select or Selector to save rebuilds
The fix: replace the list instead of mutating it: _items = [..._items, item]; the old value stays ['milk'], the new one is ['milk', 'bread'], so they differ
Close the hole: treat state as immutable: every change assigns a new list, map or object; freezed models with unmodifiable collections make an in-place add() throw
Catch it: a widget test that calls add(), pumps and expects find.text to find the new item
ProxyProvider builds a value from other providers and calls update again whenever one of them changes. For a notifier, update must reconfigure the previous instance: a new one replaces the old, which is disposed together with its state.
**create once, update many times**: update receives the dependencies and the previous value
**Plain ProxyProvider**: returning a new immutable object, such as ApiService(token), is fine: it holds no state worth keeping
**ChangeNotifierProxyProvider**: return previous!..api = api; a new instance loses the cart, and widgets that grabbed the old one with read keep calling a disposed notifier
Numbered variants: ProxyProvider2 to ProxyProvider6 take two to six dependencies
Order matters: in a MultiProvider the dependency must be listed above the proxy
**Do not call notifyListeners() inside update**: it runs during build and trips the "called during build" assertion; also, returning the same instance does not notify dependents. If a change must reach the UI, let the notifier notify from its own async method or after the frame.
Something called notifyListeners() on a notifier whose dispose() already ran: almost always an async callback that finished after the screen was gone.
Usual sources: a request that completes after the user navigated away, or a subscription or timer started in the constructor and never cancelled
**Cancel in dispose()**: every subscription and timer, before super.dispose()
Guard the async gap: set a private _disposed flag in dispose() and return early after each await; ChangeNotifier has no public getter for it
One owner: ChangeNotifierProvider(create:) disposes the notifier, so do not call dispose() on it yourself as well
Debug-only message: in release the call is a silent no-op, and the leaked timer behind it keeps running
load() calls notifyListeners() synchronously, before its first await. That marks the provider above the page dirty while the page is still being built, and Flutter forbids dirtying an ancestor during build.
Call it where the notifier is born: ChangeNotifierProvider(create: (_) => FeedNotifier(repo)..load()): nothing listens yet, so the first notify is harmless
Or after the frame: in initState via WidgetsBinding.instance.addPostFrameCallback; a bare call in initState trips the same assertion, because initState also runs during build
**Never from build: it runs many times per screen, so even without the crash you would refetch on every rebuild
Same rule for ProxyProvider.update**: assign fields there, do not notify
A dialog is a new route built by the Navigator's overlay, not by the widget that called showDialog, so its context does not descend from a provider you placed inside the page.
Same for routes: showModalBottomSheet and Navigator.push insert a route too, which is why a pushed screen cannot see the pushing screen's notifier
**useRootNavigator is true by default: the dialog lands on the top-most Navigator, even further from a nested one
Best fix for app-wide state**: provide it above the Navigator: above MaterialApp, or in MaterialApp.builder, whose child is the Navigator
For screen state: capture the object with context.read<T>() before opening the route, then wrap the dialog in ChangeNotifierProvider<T>.value(value: captured)
**.value, never create:**: create would build a second instance and dispose it on pop
Outside build: subscribe with addListener in initState and react to transitions, or model the event as data the widget consumes once. build must stay a pure function of state.
**Not in build or a Consumer**: they run on every rebuild, so the dialog opens several times, and showSnackBar during build throws the "called during build" assertion
**Not in didChangeDependencies**: it fires for any inherited change: MediaQuery, Theme, locale
Pattern 1: a listener: addListener in initState, removeListener in dispose, and compare with the previous value so you react to a change, not to every notification
Pattern 2: events as data: a Stream of events, or a nullable field the widget clears once handled
Rule of thumb: errorMessage is state to render; "show the error now" is an event
dispose() looks the notifier up through context, but by then the element is deactivated and may no longer search its ancestors. The lookup throws, so removeListener never runs and the notifier keeps calling _onChanged for a dead page.
The error: in debug: "Looking up a deactivated widget's ancestor is unsafe"; in release the lookup finds nothing and read throws instead
Why: the element is deactivated before dispose() runs, and deactivation drops its map of inherited widgets, so context.read has nothing to search
The leak: _onChanged stays registered; the next notification runs _onChanged on an unmounted State, and its context.read / ScaffoldMessenger.of(context) throws again
The fix: read the notifier once in initState (or didChangeDependencies), keep it in a field and use that field in dispose()
Rule: no context lookups in dispose(); everything it needs is captured earlier
At two levels: the notifier on its own as plain Dart with mocked dependencies, and the screen in a widget test with the notifier injected through ChangeNotifierProvider.value.
Unit-test the notifier: call its methods and assert its fields; no widget tree, no BuildContext, the fastest feedback
Widget-test the screen: wrap it in ChangeNotifierProvider<T>.value(value: notifier) above MaterialApp, pump, and assert on what is rendered
Prefer the real notifier with mocked services: via mocktail or mockito; a stubbed notifier tests the stub, not your logic
**pumpAndSettle(): after an async action, so the tree is idle before you assert
Several providers**: rebuild the same MultiProvider in the test with fakes, so the wiring between providers is tested too
Riverpod is the redesign by the same author: providers are global typed objects, not looked up through the widget tree by type, so there is no ProviderNotFoundException and no BuildContext requirement. For new code pick Riverpod; on a working Provider app, stay.
What Riverpod fixes
Run-time lookup: context.read<T>() throws ProviderNotFoundException only when it runs; a Riverpod provider is a top-level typed object that cannot go missing
**BuildContext everywhere**: services and plain Dart tests have none; Riverpod reads through Ref or a ProviderContainer
Two providers of one type: Provider finds the nearest by type, so a second Repository shadows the first; Riverpod tells providers apart by identity
Manual lifetimes: Provider state lives while its widget is mounted; autoDispose frees a provider once nothing watches it
Combining and async: a chain of ProxyProviders becomes ref.watch inside another provider, and AsyncValue gives loading, error and data as one type; family adds parameterised providers
When Provider is still fine
Small app, simple state: a few notifiers and the team knows them
Existing codebase: migration is screen by screen, not a find-and-replace, and both packages can live side by side meanwhile
watch<T>() registers your element as a dependent of the provider's inherited element, which itself listens to the value. When the value notifies, the provider element rebuilds in the next frame and marks each dependent dirty.
The lookup is O(1): dependOnInheritedWidgetOfExactType reads a map of inherited elements that Flutter copies down at mount time; nothing walks the tree
Exact type: ChangeNotifierProvider(create: (_) => AuthNotifierImpl()) is registered as AuthNotifierImpl, so reading AuthNotifier throws ProviderNotFoundException; write ChangeNotifierProvider<AuthNotifier>(...)
**read<T>(): the same lookup without registering a dependency, which is why it never rebuilds you
watch is build-only**: from a tap handler provider asserts; from initState Flutter asserts that inherited lookups must wait for didChangeDependencies
**select: registers the dependency with a selector; when notifying, the provider re-runs it and skips dependents whose slice is unchanged or which are already dirty
Many notifies, one rebuild**: the provider element is marked dirty once per frame, however many times the value notifies
Destroy the subtree that owns them instead of resetting each notifier by hand: put user-scoped providers under a widget keyed by the user id, so a new id unmounts the old ones, disposes them and runs create again.
Why the state survives: create runs once per provider element, and dispose fires only when the provider leaves the tree, which never happens above MaterialApp
Keep the root session-independent: the HTTP client, the auth notifier, analytics, config
The session boundary: a widget that wraps the signed-in part of the app, with KeyedSubtree(key: ValueKey(userId)) around the user providers
Where to put it: in MaterialApp.builder (builder: (context, child) => SignedInScope(child: child!)), so it sits above the Navigator; changing the key also remounts the Navigator and clears the route stack, which is what you want on sign-out
**Why a manual reset() is weaker: it is a checklist, and the one notifier somebody forgets leaks the previous account's data
In-flight requests**: notifiers must check a _disposed flag after await, since the key can change mid-request
Provider only clears memory: tokens, cached responses, local databases and the image cache need their own teardown
Usually one rebuild per dependent: a notification only marks things dirty, and a dirty element is rebuilt once in the next frame. The real cost is ten synchronous passes over every listener, and not all listeners are widgets.
**notifyListeners() is synchronous: every listener runs to completion, in registration order, before the call returns
Why widgets rebuild once**: the provider's listener only marks its element dirty; repeated calls return early, and dependents and select selectors are processed once when that element rebuilds
Other listeners run ten times: an addListener callback, analytics, persistence or a sync service may save or send ten times
They also see half-updated state: after the third notify, fields four to ten still hold old values, so a listener can act on an inconsistent snapshot
Rule: mutate everything, then notify once at the end, or replace one immutable state object in a single assignment
Pushing a route does not unmount the screens under it. With maintainState: true, the default, they stay mounted and subscribed, so every tick rebuilds each watching screen in the stack, not just the visible one.
Hidden is not unmounted: the Overlay keeps an entry per route; a covered route is not painted and its tickers are paused, but its elements are still in the tree
Build ignores visibility: an inherited dependency change marks those elements dirty, and the build phase rebuilds every dirty element
So cost grows with depth: five heavy screens deep, a 1 Hz notifier runs five expensive build methods a second; drive it from an animation and it is every frame
Why it surfaces late: a short test session keeps one or two screens in the stack; real users go five or six deep
Fixes, in order: narrow with select, scope the ticking notifier to the route that needs it, split a coarse notifier, and only for a heavy route that is cheap to rebuild consider maintainState: false