With riverpod_generator, how do extra parameters on a @riverpod function or a Notifier's build method turn a provider into a family?
answer
- any parameter after Ref
- named, optional and defaults allowed
- generated call mirrors the signature
- arguments stored as a record
- build params exposed as getters
basics
~20 sAny parameter after the Ref of an annotated function, or any build() parameter of an annotated Notifier, makes the generated provider a family whose call mirrors that parameter list, including named, optional and defaulted parameters and type parameters.
solid answer
~40 sHand-written families take one argument, so several values get packed into a record or class. With riverpod_generator, parameters after `Ref` on a `@riverpod` function, or on a notifier's `build`, turn the generated variable into a *family* object whose `call` mirrors the signature: `workoutsForWeekProvider(38, type: 'run')`. Positional, named, `required`, optional and defaulted parameters all work, and so do type parameters (`sampleProvider<int>()`). Internally the arguments are stored as a record such as `(int, {String? type})`, and the generated provider compares that record in `==`, so equal arguments share one cached state. In a class-based family the generated `_$` base exposes a getter per `build` parameter, so methods can read `exerciseId` directly. Arguments still need a consistent `==`, which riverpod_lint's `provider_parameters` rule checks.
code
dart · 29 linesimport 'package:riverpod_annotation/riverpod_annotation.dart';
part 'families.g.dart';
// A functional family with a positional and a named parameter.
@riverpod
Future<List<String>> workoutsForWeek(
Ref ref,
int week, {
String? type,
}) async {
final all = <String>['run', 'row', 'run'];
return type == null ? all : all.where((w) => w == type).toList();
}
// A class-based family: build's parameter becomes a getter.
@riverpod
class SetLog extends _$SetLog {
@override
List<int> build(String exerciseId) => const [];
void addSet(int reps) => state = [...state, reps];
String describe() => '$exerciseId: ${state.length} sets';
}
// Usage:
// ref.watch(workoutsForWeekProvider(38, type: 'run'));
// ref.read(setLogProvider('squat').notifier).addSet(5);go deeper
Recall that adding parameters after Ref makes a family, and that you call the provider with those arguments when reading it.
Explain the generated family object and its call method, the record that stores arguments, the generated getters in class-based families, and why arguments need a consistent ==.
Show you prevent state loss and refetch loops from unstable arguments, use provider_parameters in review, and pick named parameters over records for readable call sites.
Weigh how finely to parameterise shared state: many family members cost memory and requests, while coarse providers cost rebuilds; set naming options so families are recognisable.
## From parameters to a family A **family** is a provider that keeps a separate state per argument: one workout list per week, one set log per exercise. With hand-written Riverpod 3 providers you declare one with `.family` — for example `FutureProvider.family<List<String>, int>` — and receive a **single** argument; several values have to be packed into a record or a class. riverpod_generator removes that limit. **Any parameter after the `Ref` of a functional provider, or any parameter of a Notifier's `build` method, turns the generated provider into a family**, and the parameter list keeps its full Dart shape: - several positional parameters; - named parameters, `required` or optional; - default values; - type parameters: `List<T> sample<T extends num>(Ref ref)` is read as `sampleProvider<int>()`. ## What the generator emits For `@riverpod Future<List<String>> workoutsForWeek(Ref ref, int week, {String? type})`, the generated `workoutsForWeekProvider` is **not a provider but a family object** with a `call` method that mirrors the parameter list. Calling it returns a provider: ```dart ref.watch(workoutsForWeekProvider(38, type: 'run')); ``` Internally the arguments are stored as a Dart **record** — `(int, {String? type})` here — and the generated provider class overrides `==` and `hashCode` to compare that record. Two calls with equal arguments therefore return equal providers and share one cached state; different arguments get independent states, each with its own lifetime (auto-dispose by default under codegen). Passing the family itself to `ref.watch` without calling it does not compile, because a family is not a listenable provider. ## Class-based families For a Notifier, the parameters go on `build`: ```dart @riverpod class SetLog extends _$SetLog { @override List<int> build(String exerciseId) => const []; void addSet(int reps) => state = [...state, reps]; } ``` The generated `_$SetLog` base class keeps the argument and exposes **one getter per `build` parameter**, so any method can read `exerciseId` without storing its own copy. Widgets reach the notifier per argument: `ref.read(setLogProvider('squat').notifier).addSet(5)`. ## Equality still decides identity Because a family member is found by argument equality, arguments need a **consistent `==`**. Records, strings, numbers, enums and `DateTime` values qualify; a fresh `List` literal or an instance of a class without an `==` override does not, so building a new one on every rebuild asks for a different provider each time and the cached state is lost. riverpod_lint's `provider_parameters` rule flags such arguments, for example a non-const list literal. The deeper equality and disposal rules of families belong to the auto-dispose and families topic. ## Naming and options | Build option | Default | Applies to | |---|---|---| | `provider_name_prefix` / `provider_name_suffix` | `''` / `Provider` | providers without parameters | | `provider_family_name_prefix` / `provider_family_name_suffix` | `''` / `Provider` | providers with parameters | | `provider_name_strip_pattern` | `Notifier$` | both | The family options take precedence for providers with parameters, so a team can give families a distinct suffix if it wants call sites to show the difference. The annotation's `keepAlive`, `retry` and `dependencies` options are declared once and apply to every member of the family. ## Designing the parameter list Because the generated call mirrors your signature, the parameter list becomes public API that every widget writes out. A few habits keep it readable and stable: - put the identity of the thing first as a positional parameter (`userId`, `exerciseId`) and qualifiers as named ones (`days`, `type`); - give optional qualifiers sensible defaults, so the common call stays short: `stepsProvider('u1')` for the default window, `stepsProvider('u1', days: 30)` for a report; - prefer value types with a reliable `==` — strings, ints, enums, records or classes that override `==` — over collections and ad-hoc objects; - keep the list small: every combination of arguments is a separate cached state, so a free-text filter as a parameter creates one state per keystroke. When a provider needs many inputs that other providers already expose, it is often better to `ref.watch` those providers inside the body than to pass their values as arguments. ## Common mistakes 1. Treating `workoutsForWeekProvider` as a provider and watching it without arguments — it is a family, so the call is required. 2. Packing arguments into a record out of habit from hand-written families, which makes call sites less readable than named parameters. 3. Passing a freshly built list or map as an argument, which creates a new family member on every rebuild and loses the cached state. 4. Copying the `build` argument into a mutable field instead of reading the generated getter.
- How is a generic @riverpod provider read from a widget?Type parameters behave like arguments: they make the generated provider a family-like function. A provider declared as `List<T> sample<T extends num>(Ref ref)` is read with `ref.watch(sampleProvider<int>())`, and the type argument is part of the provider's identity, so `sampleProvider<int>()` and `sampleProvider<double>()` hold separate states.
- What is the difference between watching setLogProvider('squat') and setLogProvider('squat').notifier?Watching `setLogProvider('squat')` gives that family member's state, the list of reps, and rebuilds when it changes. `.notifier` gives the `SetLog` instance for the same argument, whose methods such as `addSet` change that state. Each argument has its own notifier instance and its own state.
saying these in an interview costs you the question
- Generated families, like hand-written ones, accept only one positional argument.
- Named and defaulted parameters are ignored when forming the family key.
- A class-based family must copy build's argument into a field to use it later.
- Passing a new list literal each rebuild is fine because generated families compare by content.
- The family variable can be passed to ref.watch without calling it.