skip to content

With riverpod_generator, how do extra parameters on a @riverpod function or a Notifier's build method turn a provider into a family?

level: middleimportance: should knowfreq 40%

answer

  1. any parameter after Ref
  2. named, optional and defaults allowed
  3. generated call mirrors the signature
  4. arguments stored as a record
  5. build params exposed as getters

basics

~20 s

Any 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 s

Hand-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 lines
dart
import '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

for a junior

Recall that adding parameters after Ref makes a family, and that you call the provider with those arguments when reading it.

for a middle

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 ==.

for a senior

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.

for a principal

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.