skip to content

Ref Watch, Read & Listen

Ref and WidgetRef are how code reads providers: watch rebuilds on change, read takes a one-off value, listen runs side effects. Interviewers probe where each belongs and what select narrows.

part ofFlutter Riverpodoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Riverpod 3, what is the difference between ref.watch, ref.read and ref.listen, and where does each one belong?

level: juniorimportance: must knowfreq 78%

answer

  1. subscribe, snapshot, or callback
  2. watch at the root of build
  3. read inside onPressed
  4. listen for snackbars and navigation
  5. listener gets previous and next

basics

~20 s

ref.watch returns a provider's value and subscribes, so the widget or provider rebuilds on change; ref.read returns the current value once with no subscription, for event handlers; ref.listen runs a callback on change for side effects like a snackbar.

solid answer

~40 s

`ref.watch(p)` is the declarative dependency: it returns the value and re-runs the caller - a widget's `build` or another provider's body - whenever `p` notifies. It belongs at the root of `build` or a builder. `ref.read(p)` returns the current value without subscribing; it belongs in callbacks such as `onPressed`, typically `ref.read(p.notifier).someMethod()`. Using `read` in `build` to 'avoid rebuilds' is the classic mistake: the UI silently goes stale once the value changes. `ref.listen(p, (previous, next) {...})` subscribes without rebuilding and calls you on change, which is where snackbars, dialogs and navigation go. In a widget, `ref.listen` is registered in `build` and removed automatically; outside `build`, such as `initState`, you use `ref.listenManual`.

code

dart · 37 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CurrencySetting extends Notifier<String> {
  @override
  String build() => 'USD';

  void change(String code) => state = code;
}

final currencyProvider =
    NotifierProvider<CurrencySetting, String>(CurrencySetting.new);

class PortfolioHeader extends ConsumerWidget {
  const PortfolioHeader({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final currency = ref.watch(currencyProvider);

    ref.listen(currencyProvider, (previous, next) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Prices now shown in $next')),
      );
    });

    return Row(
      children: [
        Text('Portfolio ($currency)'),
        TextButton(
          onPressed: () => ref.read(currencyProvider.notifier).change('EUR'),
          child: const Text('Switch to EUR'),
        ),
      ],
    );
  }
}

go deeper

for a junior

Recall the one-line rule: watch to display, read in callbacks, listen for side effects. Be ready to point at an onPressed and say which call goes there.

for a middle

Explain what each call does to the dependency graph: watch subscribes and re-runs the caller, read does not, listen subscribes without rebuilding. Mention listenManual for initState.

for a senior

Show you catch the stale-UI bug of read-in-build in review, and push rebuild reduction toward select or smaller providers rather than read.

for a principal

Frame watch-by-default as a team convention that keeps derived state correct as requirements change, and decide when a riverpod_lint rule should enforce it.

## Three ways to consume a provider In Riverpod 3 every provider is consumed through a **ref**: `Ref` inside providers and notifiers, `WidgetRef` inside widgets. Both expose the same three verbs, and choosing the wrong one is the most common Riverpod bug in code review. | Method | Returns | Subscribes? | Belongs in | |---|---|---|---| | `ref.watch(p)` | current value | yes, caller rebuilds | root of `build`, root of a builder, provider bodies | | `ref.read(p)` | current value | no | event handlers, notifier methods | | `ref.listen(p, cb)` | nothing in widgets, a `ProviderSubscription` in providers | yes, but calls `cb` instead of rebuilding | side effects: snackbar, dialog, navigation, logging | ## ref.watch: the declarative dependency `ref.watch` is Riverpod's defining feature. In a widget it works like `Theme.of(context)`: the widget subscribes and rebuilds when the provider notifies. Inside a provider it does the same for the provider itself - when a watched dependency changes, the provider's body re-runs. In a stock-portfolio app, a `portfolioValueProvider` that watches `currencyProvider` recomputes by itself when the user switches from USD to EUR; nobody has to remember to refresh it. The documentation is precise about placement: - **Good**: at the root of a `ConsumerWidget.build`, or at the root of a builder such as `ListView.builder`'s `itemBuilder`. - **Bad**: inside `initState`, or inside an `onPressed` callback. The docs also call `watch` the go-to choice even for values that never change, because it keeps the code correct if they start changing later. ## ref.read: a one-off value `ref.read` returns the current value without subscribing. Its natural home is an event handler: ```dart TextButton( onPressed: () => ref.read(currencyProvider.notifier).change('EUR'), child: const Text('Switch to EUR'), ) ``` The `.notifier` accessor of a `NotifierProvider` hands back the `Notifier` instance so the handler can call a method on it. Inside a provider, `read` is for values the provider must not rebuild on; the source comments say to prefer `watch` whenever possible. **The anti-pattern**: calling `ref.read` in `build` to render a value because it 'never changes' or to save a rebuild. It works until the value does change, at which point the screen keeps showing the old one. To cut rebuilds, the documented fixes are `select` or splitting the provider, not `read`. ## ref.listen: side effects on change `ref.listen(provider, (previous, next) {...})` registers a callback that receives the previous value (nullable, because there may not have been one) and the next value. It never makes the widget rebuild, which is exactly why it is the right place for imperative work that must happen once per change: 1. Show a `SnackBar` saying prices are now shown in EUR. 2. Navigate away when a session provider turns to logged-out. 3. Show an error dialog when an async provider moves into an error state. A widget's `ref.listen` is called at the root of `build`. The subscription is recreated on each build and removed automatically, so there is no dispose code. In debug builds `WidgetRef.listen` asserts it runs during build. For `initState`, `ConsumerState` offers `ref.listenManual`, which returns a `ProviderSubscription`, accepts `fireImmediately` (false by default) and is closed when the widget unmounts. `WidgetRef.listen` has no `fireImmediately`, since it is re-registered on every build. Inside providers `Ref.listen` also exists; it takes `fireImmediately` and `weak` (both false by default) and returns a `ProviderSubscription` you may close early. ## Choosing in practice - The value appears on screen or feeds a computation: `watch`. - The value is needed once, because the user did something: `read`. - Something must happen when the value changes, but nothing to render: `listen`. - The widget only cares about one field of a large state: `watch` with `select`. A useful review heuristic: `read` inside `build` outside a callback, and `watch` inside a callback, are both almost always wrong.

  • Can a provider use ref.watch on another provider, and what happens when that dependency changes?
    Yes, and it is the recommended way to combine providers. When a watched dependency notifies, Riverpod disposes the provider's current state - running its `onDispose` callbacks - and re-runs its body with a fresh `Ref`. A `portfolioValueProvider` watching `currencyProvider` therefore recomputes automatically on a currency switch, and every widget watching the portfolio value rebuilds with it.
  • How do you react to a provider change from initState in a ConsumerStatefulWidget?
    Use `ref.listenManual` rather than `ref.listen`. `WidgetRef.listen` is designed for the root of `build` and asserts that it is running during build. `listenManual` returns a `ProviderSubscription`, accepts `fireImmediately` (false by default) and is closed automatically when the widget unmounts, so no dispose code is needed.
  • Why does WidgetRef.listen not offer fireImmediately?
    Because it is re-registered on every build. The source notes that Riverpod could not tell which `listen` call survived between rebuilds, and firing on every rebuild would be wrong. If you need the current value at registration time, use `listenManual(..., fireImmediately: true)` from `initState`, or simply `watch` the value in `build`.

saying these in an interview costs you the question

  • Using ref.read in build is a safe way to avoid rebuilds
  • ref.watch belongs inside onPressed so the handler sees fresh data
  • ref.listen returns the value, so it can replace ref.watch
  • Showing a dialog directly from a watched value inside build is fine
  • A widget's ref.listen must be removed by hand in dispose
  • Providers cannot watch other providers, only widgets can
open as a page

In flutter_riverpod 3, how do ConsumerWidget, ConsumerStatefulWidget and Consumer each give you a WidgetRef, and when do you pick each?

level: juniorimportance: should knowfreq 58%

basics

~20 s

ConsumerWidget passes a WidgetRef as build's second parameter; ConsumerStatefulWidget's ConsumerState exposes a ref field usable in every State lifecycle; Consumer is a builder widget that hands (context, ref, child) to a small subtree so only it rebuilds.

open as a page

In Riverpod 3, what is the difference between ref.invalidate and ref.refresh, and when would you pass asReload: true?

level: middleimportance: should knowfreq 42%

basics

~20 s

ref.invalidate discards a provider's state now and lets it rebuild later, coalescing repeated calls; ref.refresh is invalidate followed by read, rebuilding immediately and returning the new value. asReload: true makes an async provider drop its previous data while reloading.

open as a page

In Riverpod 3, how does provider.select narrow a widget's rebuilds, and what does it not save you from?

level: middleimportance: should knowfreq 46%

basics

~20 s

provider.select(fn) makes ref.watch or ref.listen react only when fn's result changes by ==. The upstream provider still recomputes and the selector still runs on every change; returning a freshly built list or object each time defeats it.

open as a page

A Riverpod 3 FutureProvider that watches the currency setting sometimes throws UnmountedRefException after an await when the user switches currency mid-request; what is happening and how do you fix it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Switching currency rebuilds the provider with a new Ref, so the closure still awaiting from the previous build holds a disposed Ref; using it throws UnmountedRefException. Cancel the work in ref.onDispose, or check ref.mounted after each await.

open as a page