In a Flutter survey app using the provider package, SurveyPage provides SurveyAnswers but the ReviewPage it pushes throws ProviderNotFoundException; why, and how do you fix it?
answer
- routes are children of the Navigator
- pushed page is not a descendant
- the 'different route' scenario
- re-provide with .value, not create
- or lift above the Navigator
basics
~20 sA pushed route is a child of the Navigator, not of SurveyPage, so lookups from ReviewPage never pass SurveyPage's provider. Lift the provider above the Navigator or around the flow, or re-provide the same instance on the new route with ChangeNotifierProvider.value.
solid answer
~40 sProvider lookups walk up from the reading widget, and `Navigator.push` inserts the new route as a child of the `Navigator`, a **sibling** of SurveyPage's route rather than a descendant. So `ReviewPage`'s `context.watch<SurveyAnswers>()` never passes the provider and throws; the exception even lists 'the provider you are trying to read is in a different route'. Fixes, by lifetime: lift the provider above the Navigator (in `runApp` or `MaterialApp`'s `builder`) if it is app-wide; scope it around the flow with a nested navigator or a go_router `ShellRoute` whose builder wraps its child; or read the instance before pushing and wrap the new page in `ChangeNotifierProvider.value(value: answers, ...)`. Use `.value` there, not `create: (_) => answers`, or popping ReviewPage would dispose a notifier SurveyPage still uses.
code
dart · 27 linesimport 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// SurveyAnswers is a ChangeNotifier provided by SurveyPage.
class ReviewButton extends StatelessWidget {
const ReviewButton({super.key});
@override
Widget build(BuildContext context) {
return FilledButton(
onPressed: () {
// read while this context is still below SurveyPage's provider
final answers = context.read<SurveyAnswers>();
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ChangeNotifierProvider.value(
value: answers, // .value: popping ReviewPage does not dispose it
child: const ReviewPage(),
),
),
);
},
child: const Text('Review answers'),
);
}
}go deeper
Remember that a pushed page cannot see a provider created inside the page that pushed it.
Explain the Navigator hosting routes as its own children, and the re-provide pattern with ChangeNotifierProvider.value read before the push.
Choose the fix by lifetime (app-wide lift, flow scoping with a nested navigator or ShellRoute, or .value re-provide) and explain the dispose-while-in-use bug of the create variant.
Decide a team-wide policy for multi-route flows so state has one owner per flow, instead of ad hoc lifts to the root that hide lifetime bugs.
## Why the pushed route cannot see it In the provider package, a read such as `context.watch<SurveyAnswers>()` searches the **ancestors** of the calling widget. The failure comes from where routes live. `Navigator.push(context, MaterialPageRoute(builder: (_) => const ReviewPage()))` does not insert `ReviewPage` under the widget that called it; the `Navigator` hosts every route as its **own child**. The tree looks like this: ``` MaterialApp Navigator route 1: SurveyPage ChangeNotifierProvider<SurveyAnswers> survey steps (can read SurveyAnswers) route 2: ReviewPage (ancestors: Navigator, MaterialApp) ``` Walking up from `ReviewPage` reaches the `Navigator` and `MaterialApp` but never `SurveyPage`'s provider, so provider throws `ProviderNotFoundException`. The debug message lists this case explicitly: 'The provider you are trying to read is in a different route. Providers are "scoped". So if you insert of provider inside a route, then other routes will not be able to access that provider.' (the grammar is the package's). The same applies to dialogs: `showDialog` pushes onto the **root** navigator by default (`useRootNavigator: true`), so a dialog opened from inside SurveyPage cannot see SurveyPage's provider either. ## The fixes, by lifetime | Fix | How | Lifetime of SurveyAnswers | Watch out for | |---|---|---|---| | Lift above the Navigator | provider in `runApp` above `MaterialApp`, or in `MaterialApp`'s `builder`, which inserts widgets above the Navigator | whole app | answers survive after the survey ends; reset them yourself | | Scope the flow | a nested `Navigator` for the survey steps inside the provider, or a go_router `ShellRoute` whose `builder` wraps its `child` in the provider | exactly the survey flow | the review page must be a route of that flow | | Re-provide the instance | read it before pushing, wrap the new page in `ChangeNotifierProvider.value` | owned by SurveyPage | must be `.value`, never `create` | | Pass it in | `ReviewPage(answers: answers)` and listen with `ListenableBuilder` | owned by SurveyPage | loses the provider API on that page | ## Re-providing correctly The re-provide fix is the most common one in interviews, and it has one sharp edge. Compare: 1. `ChangeNotifierProvider.value(value: answers, child: const ReviewPage())`: exposes the existing instance and, when ReviewPage pops, only stops listening. SurveyPage's own provider disposes `answers` later, when SurveyPage leaves the tree. 2. `ChangeNotifierProvider(create: (_) => answers, child: const ReviewPage())`: tells the new provider it **owns** `answers`, so popping ReviewPage calls `answers.dispose()`. Back on SurveyPage, the next `notifyListeners()` hits Flutter's debug assertion 'A SurveyAnswers was used after being disposed.' Read the instance with `context.read<SurveyAnswers>()` in the handler, before `Navigator.push`, while the context is still SurveyPage's. ## Scoping the flow with go_router With go_router 18, a `ShellRoute` builds a shell around a nested navigator and hands that navigator to its `builder` as `child`. Wrapping `child` in the provider makes every route of the shell a descendant of it: ```dart final router = GoRouter( routes: [ ShellRoute( builder: (context, state, child) => ChangeNotifierProvider( create: (_) => SurveyAnswers(), child: child, ), routes: [ GoRoute(path: '/survey', builder: (context, state) => const SurveyPage()), GoRoute(path: '/survey/review', builder: (context, state) => const ReviewPage()), ], ), ], ); ``` Navigating from `/survey` to `/survey/review` stays inside the shell, so `ReviewPage` reads the same `SurveyAnswers`. Leaving the shell unmounts the provider and disposes the answers. ## Choosing - If other parts of the app need the answers, lift it. - If only the survey's screens need them and the survey spans several routes, scope the flow; this also disposes the answers automatically when the flow closes. - If one extra page briefly needs the same object, re-provide with `.value`. - Avoid moving everything to the root just to silence the exception: that trades a loud error for state that silently lives too long. ## Diagnosing quickly - The exception names the **type** and the **widget** that asked: 'Could not find the correct Provider<SurveyAnswers> above this ReviewPage Widget'. - Check how the failing widget was reached: pushed route, dialog, bottom sheet or overlay entry; each hangs off a navigator or overlay rather than off the opener. - In release builds the message shrinks to 'Provider<SurveyAnswers> not found for ReviewPage', so reproduce in debug for the full guidance.
- What goes wrong if the pushed route uses ChangeNotifierProvider(create: (_) => answers) instead of .value?The create constructor treats the object as its own, so when ReviewPage is popped the provider calls `answers.dispose()`. SurveyPage still holds the same instance; its next `notifyListeners()` triggers Flutter's debug assertion that the notifier was used after being disposed. `.value` only removes its listener on unmount.
- A dialog opened with showDialog from inside SurveyPage cannot read SurveyAnswers either. Why?`showDialog` pushes a route, and by default onto the root navigator (`useRootNavigator: true`). The dialog's widgets are therefore descendants of that navigator, not of SurveyPage. Re-provide the instance inside the dialog's builder with `.value`, or place the provider above the navigator.
- How does scoping the survey with a go_router ShellRoute help?A `ShellRoute`'s `builder` receives the child navigator's widget, so wrapping that child in the provider puts every step route of the survey below it. All steps and the review page can read the answers, and the provider, with its answers, is disposed when the user leaves the shell.
saying these in an interview costs you the question
- A route pushed from a screen is a descendant of that screen
- Re-provide with create: (_) => answers so the new route can read it
- Moving every provider to the root is the correct fix for this exception
- ProviderNotFoundException means the provider was created lazily and not yet
- Dialogs share the provider scope of the widget that opened them