skip to content

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%

answer

  1. build gets a second parameter
  2. ConsumerState exposes a ref field
  3. builder with context, ref, child
  4. ProviderScope must sit above
  5. ref is the element itself

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.

solid answer

~40 s

All three need a `ProviderScope` above them. `ConsumerWidget` is the stateless default: `build(BuildContext context, WidgetRef ref)`. When you also need `State` - controllers, `initState`, `dispose` - extend `ConsumerStatefulWidget` and return a `ConsumerState`, which has a `ref` property; in `initState` you use `ref.read` or `ref.listenManual`, and keep `ref.watch` for `build`. `Consumer(builder: (context, ref, child) {...}, child: ...)` gives a ref to an inline subtree inside an ordinary widget, so only that subtree rebuilds, and its `child` is built once and passed through. The docs recommend extracting a separate `ConsumerWidget` first and `Consumer` when a new class is not worth it. Under the hood the `WidgetRef` is the widget's element, so using `ref` after the widget unmounts, such as in `dispose`, throws.

code

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

class CurrencyPicker extends ConsumerStatefulWidget {
  const CurrencyPicker({super.key});

  @override
  ConsumerState<CurrencyPicker> createState() => _CurrencyPickerState();
}

class _CurrencyPickerState extends ConsumerState<CurrencyPicker> {
  late final TextEditingController _code;

  @override
  void initState() {
    super.initState();
    _code = TextEditingController(text: ref.read(currencyProvider));
    ref.listenManual(currencyProvider, (previous, next) {
      _code.text = next;
    });
  }

  @override
  void dispose() {
    _code.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _code,
      onSubmitted: (value) =>
          ref.read(currencyProvider.notifier).change(value),
    );
  }
}

go deeper

for a junior

Know which base class gives ref where: build's second parameter, a ref field on ConsumerState, or Consumer's builder. Remember ProviderScope at the root.

for a middle

Explain which ref calls are allowed in initState versus build, why listenManual exists, and how Consumer's child avoids rebuilding a subtree.

for a senior

Spot ref misuse in dispose and WidgetRef leaking into non-widget classes, and choose between extracting a ConsumerWidget and inlining a Consumer on readability grounds.

for a principal

Set the team convention for where WidgetRef may appear, keeping widgets thin and business logic in providers that tests can drive without a widget tree.

## Where a WidgetRef comes from A `WidgetRef` is how widget code reads providers. Riverpod 3 offers three widget types that supply one, and all of them look up the nearest **`ProviderScope`**. Without one above them, the lookup throws a `StateError` with the message `No ProviderScope found`, which is why `runApp(const ProviderScope(child: MyApp()))` is the first line of every Riverpod app. | Widget | How you get the ref | Has State lifecycles? | Typical use | |---|---|---|---| | `ConsumerWidget` | second parameter of `build(context, ref)` | no | most screens and components | | `ConsumerStatefulWidget` + `ConsumerState` | `ref` field on the State | yes | controllers, `initState`, `dispose` | | `Consumer` | `builder: (context, ref, child)` | no | an inline subtree inside a non-consumer widget | ## ConsumerWidget The stateless default. You extend `ConsumerWidget` instead of `StatelessWidget`, and `build` gains a `WidgetRef ref` parameter: ```dart class HoldingsTotal extends ConsumerWidget { const HoldingsTotal({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final currency = ref.watch(currencyProvider); return Text('Total in $currency'); } } ``` In the 3.4 source, `ConsumerWidget` is itself implemented as a `ConsumerStatefulWidget` whose private State forwards to your `build`, so the two share one element type. ## ConsumerStatefulWidget and ConsumerState When a widget owns a `TextEditingController`, an `AnimationController` or needs `initState`, extend `ConsumerStatefulWidget` and have `createState` return a subclass of `ConsumerState`. That State has every normal `State` lifecycle plus a `ref` property. Rules of thumb: - In `initState`, use `ref.read` for a one-off value and `ref.listenManual` for a subscription; `ref.watch` and `ref.listen` belong in `build`. - Subscriptions from `listenManual` are closed automatically when the widget unmounts. - Do not use `ref` in `dispose`. The ref relies on the element's `BuildContext`, and the source throws a `StateError` saying that using `ref` when a widget is about to be, or has been, unmounted is unsafe. Its advice: save the provider value in a field of your State if `dispose` needs it. ## Consumer `Consumer` is a widget with a `builder` and an optional `child`. It exists for one reason: to shrink what rebuilds without writing a new class. In a portfolio screen whose `Scaffold`, `AppBar` and chart do not depend on the currency, wrapping only the price label in a `Consumer` means a currency switch rebuilds the label alone: ```dart Consumer( builder: (context, ref, child) { final currency = ref.watch(currencyProvider); return Row(children: [Text(currency), child!]); }, child: const Icon(Icons.currency_exchange), ) ``` The `child` is created once by the parent and handed to the builder on every rebuild, so an expensive but provider-independent subtree is not rebuilt. The docs rank the options: extracting a separate `ConsumerWidget` is recommended; `Consumer` is less recommended but avoids a new widget class. ## How it works underneath The element that backs these widgets, `ConsumerStatefulElement`, implements `WidgetRef` itself; `ConsumerState.ref` is simply the context cast to `WidgetRef`. Consequences worth knowing: 1. `ref.watch` subscriptions are recorded per build; dependencies not watched again in the next build are closed. 2. When the `ProviderScope` above the widget changes container, existing subscriptions are closed and re-established. 3. The docs state that a `WidgetRef` should not leave the widget layer. Code outside widgets - repositories, services - should receive a provider `Ref` or plain values instead. ## Choosing - Pure display from providers: `ConsumerWidget`. - Needs controllers or lifecycle hooks: `ConsumerStatefulWidget`. - One small reactive island in an otherwise static tree: `Consumer`, or better, extract a small `ConsumerWidget`.

  • Why does using ref inside State.dispose throw in flutter_riverpod 3?
    Because the `WidgetRef` is the widget's element and relies on its `BuildContext`, which is no longer safe once the widget is deactivated. The element checks `context.mounted` and throws a `StateError`. If `dispose` needs a provider value, read it earlier - in `initState` or `build` - and store it in a field.
  • When would you choose Consumer over extracting a new ConsumerWidget?
    When the reactive part is tiny and a new class adds more noise than it saves, for example one label inside a large `Scaffold`. The documentation prefers extracting a `ConsumerWidget` because it reads better and is easier to test, and treats `Consumer` as the lighter fallback. Either way only that subtree rebuilds when the watched provider changes.

saying these in an interview costs you the question

  • ConsumerWidget works without a ProviderScope above it
  • ref.watch in initState is the right way to read at startup
  • It is fine to use ref in dispose to clean up a provider
  • A WidgetRef can be passed into a repository class for later use
  • Consumer's child parameter lists the providers it watches