skip to content

With the provider package, why does context.watch fail inside onPressed or initState, and what should those call sites use instead?

level: middleimportance: must knowfreq 58%

answer

  1. subscriptions only make sense while building
  2. listen: true outside build asserts
  3. initState runs before dependencies are allowed
  4. read works in handlers and initState
  5. select is build-only

basics

~20 s

context.watch subscribes the widget, which is only meaningful during build: in onPressed provider's debug assertion rejects listening, and in initState Flutter rejects the dependency. Use context.read, or Provider.of with listen: false, at both call sites.

solid answer

~40 s

`context.watch<T>()` is `Provider.of<T>(context)` with `listen: true`, and a subscription only helps where a change can trigger another `build`. In an event handler such as `onPressed`, the build owner is not building, so provider's debug assertion fires: 'Tried to listen to a value exposed with provider, from outside of the widget tree', with the hint to pass `listen: false`. In `initState` the call reaches Flutter's `dependOnInheritedWidgetOfExactType`, which asserts it was called before `initState()` completed, because `initState` never runs again to use the dependency. Both sites should call `context.read<T>()` (or `Provider.of<T>(context, listen: false)`). `context.select` is stricter still: only inside `build` or a `LayoutBuilder` builder.

code

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

// Basket is a ChangeNotifier with load(), add(sku) and checkout().

class BasketPage extends StatefulWidget {
  const BasketPage({super.key});

  @override
  State<BasketPage> createState() => _BasketPageState();
}

class _BasketPageState extends State<BasketPage> {
  @override
  void initState() {
    super.initState();
    // read is allowed here; the mutation is deferred past this build
    Future.microtask(() {
      if (mounted) context.read<Basket>().load();
    });
  }

  @override
  Widget build(BuildContext context) {
    final count = context.select((Basket b) => b.count);
    return ElevatedButton(
      onPressed: () async {
        await context.read<Basket>().checkout();
        if (!context.mounted) return;
        Navigator.of(context).pop();
      },
      child: Text('Checkout $count items'),
    );
  }
}

go deeper

for a junior

Remember the rule: watch and select in build, read in callbacks such as onPressed and in initState.

for a middle

Explain the two different assertions: provider's listen check in handlers and Flutter's dependency check before initState completes, and why both point to listen: false.

for a senior

Diagnose the notify-during-build error that follows a legal read in initState, and choose between moving work into create and deferring with a microtask; add mounted checks after awaits.

for a principal

Encode the call-site rules in lint or review guidelines so async handlers, initState side effects and read-in-build are caught before they reach a release build where asserts are stripped.

## Subscribe while building, read everywhere else The provider package's reading methods are not interchangeable across a widget's lifecycle. `context.watch` and `context.select` **subscribe** the calling widget to a provider, and a subscription only makes sense where Flutter can act on it: during a build, where a later change can schedule another build. `context.read` merely **looks up** the value, so it works almost anywhere a live `BuildContext` exists. ## In an event handler A button's `onPressed` runs long after `build` finished, in response to a gesture. `context.watch<Basket>()` there is `Provider.of<Basket>(context)` with the default `listen: true`, and `Provider.of` begins with a **debug assertion**: listening is allowed only while the build owner is building (or inside a provider's `update` callback). Otherwise it fails with: > Tried to listen to a value exposed with provider, from outside of the widget tree. The message names the likely cause (an event handler) and the fix, `Provider.of<T>(context, listen: false)`, which is what `context.read<T>()` calls. Its stated reason: the subscription would pointlessly rebuild the widget that owns the handler, even though that widget's tree does not display the value. `context.select` in a handler hits its own assertion: it may only be used inside the `build` method of a widget. ## In initState `initState` runs once, before the first `build`. `context.watch` there calls Flutter's `dependOnInheritedWidgetOfExactType`, and Flutter's `StatefulElement` rejects that in debug mode with 'dependOnInheritedWidgetOfExactType<...>() or dependOnInheritedElement() was called before ...initState() completed.' Flutter's reasoning is that a dependency registered in `initState` is useless because `initState` is never called again to pick up the new value; it points you at `build` or `didChangeDependencies` instead. `context.read` registers no dependency, and the provider package's own test suite includes 'read in initState works'. `context.select` in `initState` throws, like `watch`. ## Where each call belongs | Call site | `watch` | `select` | `read` or `Provider.of(listen: false)` | |---|---|---|---| | `build` of a StatelessWidget or State | yes | yes | works, discouraged for displayed values | | `LayoutBuilder` builder | yes | yes | works | | Event handler (`onPressed`, `onTap`) | provider debug assertion | provider debug assertion | yes | | `initState` | Flutter debug assertion | assertion | yes | A few more placements come up in reviews: - **Provider `create` callbacks** that need another provider use `read`: `create: (context) => CheckoutModel(context.read<Basket>())`. The package docs note that `listen: false` is necessary for `Provider.of` inside `create` and `initState`. - **`didChangeDependencies`**: `read` works there (the package tests it), while `select` is documented as unsupported outside `build`. - **After an `await`** in a handler, check `context.mounted` before touching `context` again; the widget may have been unmounted while the future ran. - **`read` inside `build`** is not an error in provider 6.0.3 (the asserts that once restricted `read` and `watch` were removed in 4.3.3), but the docs call it an anti-pattern. Look the value up inside the callback instead: `onPressed: () => context.read<Basket>().add(sku)`. ## The initState mutation trap A related failure hides behind a legal `read`. Calling `context.read<Basket>().load()` in `initState`, where `load()` synchronously calls `notifyListeners()`, notifies **during the build phase**. The provider marks itself as needing to rebuild while the framework is building a descendant, and Flutter's debug check fails with 'setState() or markNeedsBuild() called during build.' The package FAQ explains why it is disallowed (some widgets would build with the old value, others with the new) and gives two fixes: 1. start the work where it affects the whole tree equally: the model's constructor or the provider's `create`; 2. defer it past the current build: `Future.microtask(() => context.read<Basket>().load())`. Option 1 suits work with no external parameter; option 2 lets the widget pass arguments at the cost of one extra frame with the old state. ## Summary - `watch` and `select`: only while building. - `read`: handlers, `initState`, `didChangeDependencies`, `create`, and anywhere outside build. - A legal `read` can still trigger a notification during build; defer mutations with a microtask or move them into `create`. - Asserts are compiled out of release builds, so a `watch` left in a handler is not reported there; it quietly registers a dependency, and the handler's widget may rebuild for nothing. Debug runs and widget tests are where these mistakes surface, which is a good reason to exercise every handler in a test. - The same split applies to the widget forms: `Consumer` and `Selector` are built widgets, so they are always in the 'while building' column.

  • context.read in initState works, yet the app throws 'setState() or markNeedsBuild() called during build'. Why?
    The read is fine; the method it calls notifies synchronously. Notifying during the build phase marks the provider dirty while a descendant is being built, which Flutter's debug check rejects. Start the work in the model's constructor or the provider's `create`, or defer it with `Future.microtask(() => context.read<Basket>().load())`.
  • Why does the provider assertion say listening from a handler is unsupported rather than merely useless?
    The assert message explains that a subscription from a handler may pointlessly rebuild the widget that owns the handler, even though its tree does not display the value. In debug builds the assertion catches the mistake; `listen: false` states the intent explicitly.
  • Where may context.select be called, and where not?
    Only while building: inside a widget's `build` method or a `LayoutBuilder` builder. It asserts in handlers, in `initState` and in `didChangeDependencies`, and also when called with the `itemBuilder` context of a `ListView.builder`, because that context belongs to the whole list.

saying these in an interview costs you the question

  • context.watch in onPressed is fine; it just rebuilds the button
  • context.read is forbidden in initState, so move everything to build
  • context.select can replace read in event handlers
  • Any context.read in initState is safe, even if it calls notifyListeners synchronously
  • The initState error comes from provider; plain InheritedWidgets allow it