In flutter_riverpod 3, how do ConsumerWidget, ConsumerStatefulWidget and Consumer each give you a WidgetRef, and when do you pick each?
answer
- build gets a second parameter
- ConsumerState exposes a ref field
- builder with context, ref, child
- ProviderScope must sit above
- ref is the element itself
basics
~20 sConsumerWidget 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 sAll 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 linesimport '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
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.
Explain which ref calls are allowed in initState versus build, why listenManual exists, and how Consumer's child avoids rebuilding a subtree.
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.
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