skip to content

Scoping & Disposal

A provider is visible only below it, so a pushed route can throw ProviderNotFoundException, and it disposes only what it created. Interviewers probe placement and why Riverpod avoids the error.

on this pageshow

explore

questions

5

With the provider package, which widgets can read a provider, and how do you decide where to place ChangeNotifierProvider in the tree?

level: juniorimportance: must knowfreq 62%

answer

  1. lookups only go upward
  2. descendants of the provider widget
  3. nearest ancestor of the type wins
  4. lowest common ancestor of the readers
  5. hot reload does not rerun main

basics

~20 s

Only descendants of a provider widget can read it, and the nearest provider of the requested type wins. Place ChangeNotifierProvider at the lowest common ancestor of every widget and route that reads it, no higher than its state should live.

solid answer

~40 s

A provider is a widget, and `context.watch`, `read` and `Provider.of` search **upward** from the calling widget, so only descendants of the provider see it; if two providers expose the same type, the closest ancestor wins. Placement is therefore a trade-off. Put the provider at the **lowest common ancestor** of everything that reads it: a screen-local form model on that screen, app-wide services in a `MultiProvider` above `MaterialApp`. Too low and siblings, dialogs or pushed routes throw `ProviderNotFoundException`; too high and the state outlives its use and must be reset by hand. Two traps come with it: reading a provider in the same `build` that creates it, whose context sits above the provider, and adding a provider in `main.dart` then hot reloading, which needs a hot restart.

code

dart · 35 lines
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// AuthSession, SurveyRepository and SurveyAnswers are app classes;
// SurveyAnswers is a ChangeNotifier.

void main() {
  runApp(
    MultiProvider(
      providers: [
        // app-wide: visible to every route and dialog
        ChangeNotifierProvider(create: (_) => AuthSession()),
        Provider(create: (_) => SurveyRepository()),
      ],
      child: const MaterialApp(home: SurveyPage()),
    ),
  );
}

class SurveyPage extends StatelessWidget {
  const SurveyPage({super.key});

  @override
  Widget build(BuildContext context) {
    // screen-scoped: created and disposed with this page
    return ChangeNotifierProvider(
      create: (_) => SurveyAnswers(),
      builder: (context, child) {
        // this context is below the provider
        final answered = context.watch<SurveyAnswers>().answered;
        return Scaffold(body: Center(child: Text('$answered answered')));
      },
    );
  }
}

go deeper

for a junior

Recall that a provider is visible only to widgets below it and that app-wide providers go above MaterialApp, screen state on the screen.

for a middle

Explain lowest-common-ancestor placement, same-type shadowing and why the build context that creates a provider cannot read it.

for a senior

Show that placement also sets lifetime: justify each provider's level by who reads it and how long its state must survive, including pushed routes and dialogs.

for a principal

Set conventions for a large codebase: which layers may hold app-wide providers, how flows scope their state, and how tests mirror production placement.

## The visibility rule In the provider package every provider (`Provider`, `ChangeNotifierProvider`, `MultiProvider` and the rest) is a **widget**. Reading one, whether with `context.watch<T>()`, `context.read<T>()` or `Provider.of<T>(context)`, asks the framework for the **nearest ancestor** of the calling widget that exposes type `T`. Three consequences follow: - Only **descendants** of the provider widget can read it. Siblings, parents and anything in another branch cannot. - If two providers expose the **same type**, the reader gets the closest one; the outer one is shadowed. The package FAQ answers 'can I obtain two different providers using the same type?' with no, and recommends distinct types instead. - The `BuildContext` used for the lookup matters, not the widget that displays the value. A context belonging to a widget **above** the provider finds nothing. When the lookup fails for a non-nullable type, provider throws `ProviderNotFoundException`, whose debug message begins 'Could not find the correct Provider<T> above this X Widget'. ## Choosing the level | Placement | Typical contents | Benefit | Cost | |---|---|---|---| | Above `MaterialApp`, in `runApp` | auth session, repositories, settings | every route, dialog and screen sees it | lives for the whole app; must be reset manually | | Around one screen | a form or filter model for that screen | created and disposed with the screen | routes pushed from the screen cannot see it | | Around a flow of screens | a multi-step form shared by its steps | lives exactly as long as the flow | needs a nested navigator or a shell route | | Around one small subtree | a model used by a single widget group | narrowest lifetime and rebuild surface | easy to place too low | The rule of thumb: place a provider at the **lowest common ancestor** of every widget that reads it, and ask how long its state should live. A provider's `create` result is disposed when the provider widget leaves the tree, so placement also decides lifetime. ## Trap 1: reading in the same build that creates it ```dart @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => SurveyAnswers(), // throws: this context belongs to the widget above the provider child: Text('${context.watch<SurveyAnswers>().answered}'), ); } ``` The `context` here belongs to the widget whose `build` is running, the **parent** of the provider. The exception message itself suggests the fix: providers accept a `builder` parameter that receives a new `BuildContext` below the provider. A `Builder`, a `Consumer` or extracting a child widget does the same. ## Trap 2: hot reload after adding a provider in main Hot reload keeps the running widget tree and does not execute `main()` again, so a provider newly added inside `runApp(...)` never reaches the tree. The exception's first listed scenario is exactly this, with the fix: perform a **hot restart**. ## Placement checklist 1. List every reader, including dialogs and routes the screen pushes. 2. Find their lowest common ancestor in the widget tree, remembering that pushed routes and dialogs hang off a `Navigator`, not off the screen that opened them. 3. Decide the lifetime the state needs; move the provider up only as far as that lifetime allows. 4. Group app-wide providers in one `MultiProvider` above `MaterialApp` to keep nesting readable. ## Symptoms of the wrong level Placement mistakes show up in two opposite ways: - **Too low**: `ProviderNotFoundException` from a sibling widget, a dialog, a bottom sheet or a pushed route; or the same model accidentally created twice, once per subtree, so two parts of the screen disagree. - **Too high**: state that survives when it should not, such as a previous user's survey answers still present after logout, or a form that reopens pre-filled; plus a crowded root that every test must reproduce. The fix for the first is to move the provider up only to the common ancestor; the fix for the second is to move it down, or to reset its state explicitly at the point where its logical lifetime ends. ## Placement and tests A widget test pumps only the widget under test, so every provider it reads must be placed above it in the test too, typically `Provider<Foo>.value(value: fakeFoo, child: const TestedWidget())`. The package docs point out that the explicit `<Foo>` matters: with a fake subclass the inferred type would be `Provider<FakeFoo>` and the widget's lookup of `Foo` would fail.

  • Two Provider<String> widgets are nested; which value does a descendant read?
    The closest ancestor's. Lookup is by type, and the inner provider shadows the outer one, so the outer value is unreachable from below it. The provider docs recommend giving each value its own type, for example `Country` and `City` wrapper classes, instead of two providers of `String`.
  • Why does a provider added to runApp in main.dart throw ProviderNotFoundException after a hot reload?
    Hot reload keeps the existing widget tree and does not rerun `main()`, so the newly added provider widget is never inserted. The exception message lists this case first; a hot restart rebuilds the app from `main()` and the provider appears.
  • Is there a practical limit to stacking providers at the root?
    The package FAQ notes that with a very large number of providers, around 150 or more, some devices can hit a `StackOverflowError` because so many widgets build at once, and suggests mounting providers over time, for example behind a splash screen. Long before that, a crowded root usually signals state placed higher than it needs to live.

saying these in an interview costs you the question

  • Any widget on the same screen can read a provider, wherever it is placed
  • Putting every provider above MaterialApp has no cost
  • With two providers of the same type, a reader can pick the outer one
  • Hot reload is enough after adding a provider to runApp
  • The context of the build method that creates a provider can read it
open as a page

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?

level: seniorimportance: must knowfreq 55%

basics

~20 s

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

open as a page

With the provider package, when does a provider dispose its value, and why is an instance passed to a .value constructor never disposed?

level: middleimportance: should knowfreq 42%

basics

~20 s

A provider disposes only what its create callback built, when the provider widget is unmounted: ChangeNotifierProvider calls dispose() itself, Provider calls its dispose callback. A .value constructor exposes an instance someone else owns, so it never disposes it.

open as a page

Why can Riverpod not throw the provider package's ProviderNotFoundException, and how does it replace provider's placement-based disposal?

level: middleimportance: should knowfreq 38%

basics

~20 s

provider's providers are widgets found by type at runtime, so a misplaced one fails the lookup. Riverpod's are global final declarations read by reference from one root ProviderScope, and state is freed by autoDispose rather than by unmounting a widget.

open as a page

In provider 6, what does context.watch<SurveyAnswers?>() return when no matching provider is found, and when is a nullable lookup appropriate?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Since provider 6.0.0, a nullable type argument such as watch<SurveyAnswers?>() or read<SurveyAnswers?>() returns null when no provider is found, instead of throwing ProviderNotFoundException. Use it for reusable widgets that genuinely work with or without the provider.

open as a page