skip to content

In Riverpod 3, when does riverpod_generator's @riverpod syntax pay off, and when do hand-written providers fit better?

level: middleimportance: must knowfreq 50%

answer

  1. codegen is optional in Riverpod
  2. already running freezed or json_serializable?
  3. macros were cancelled
  4. no provider-type choice, any parameters
  5. stateful hot reload of one provider

basics

~20 s

Riverpod's docs recommend riverpod_generator only when a project already runs code generation, such as freezed or json_serializable. It buys automatic provider kinds, any parameters, auto-dispose defaults and stateful hot reload, at the cost of a slow build step.

solid answer

~40 s

Code generation is optional in Riverpod 3; both styles produce the same runtime providers, and `ref.watch`, overrides, AsyncValue and retry work identically. Riverpod's own guidance is to use it only if you already use code generation for other things, like freezed or json_serializable: generation in Dart is still fairly slow, and the push towards it assumed Dart macros would remove the build step, but macros were cancelled. What `@riverpod` buys: no choosing between `Provider`, `FutureProvider` or `AsyncNotifierProvider`, families with any parameter list, auto-dispose by default, stateful hot reload that re-runs only the edited provider, and generator-only lint rules. Hand-written fits a project without build_runner, a small app, a gradual migration where both styles coexist, and legacy kinds like `StateNotifierProvider`, which the generator does not produce.

code

dart · 16 lines
dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'goal.g.dart';

// Hand-written: kind, lifetime and argument chosen explicitly.
final handWrittenGoalProvider =
    FutureProvider.autoDispose.family<int, String>((ref, userId) async {
  return 10000;
});

// Generated equivalent: kind and lifetime follow from the code.
@riverpod
Future<int> goal(Ref ref, String userId, {int weekOffset = 0}) async {
  return 10000;
}

go deeper

for a junior

Know that @riverpod code generation is optional and that both styles give ordinary providers you read with ref.watch.

for a middle

Explain the documented rule, adopt it when codegen already runs, and list concrete benefits and costs such as auto-dispose defaults, free parameter lists and the build step.

for a senior

Show you weigh build-time cost, stateful hot reload and lint coverage against a team's existing tooling, and plan how both styles coexist during a transition.

for a principal

Treat it as a codebase-wide convention: tooling cost across teams, consistency of lifetime defaults, and when a mixed state becomes a maintenance burden worth finishing.

## Codegen is optional Riverpod 3 offers two ways to declare providers. - **Hand-written**: `final stepsProvider = FutureProvider<int>((ref) async => 0);` — you choose the provider class, its modifiers and its argument type yourself. - **Generated**: annotate a function or a Notifier class with `@riverpod`, and riverpod_generator writes the provider into a `.g.dart` part file. Both produce the same runtime objects. `ref.watch`, `ref.read`, overrides, `AsyncValue` and automatic retry behave identically. Riverpod's documentation calls code generation completely optional while saying the project embraces and recommends it. ## The official rule of thumb The Riverpod docs answer *Should I use code generation?* with: **only if you already use code generation for other things**, citing freezed and json_serializable. The reasoning: - code generation in Dart is still fairly slow, so adding a generator purely for providers is a real cost in iteration time; - codegen was recommended most strongly while Dart **macros** were expected to remove the separate build step; macros were cancelled, so the step stays; - a project that already runs a generator is already set up, so adding riverpod_generator costs little. ## What `@riverpod` buys - **No provider-kind decision.** The return type and the function-versus-class shape select the equivalent of `Provider`, `FutureProvider`, `StreamProvider`, `NotifierProvider`, `AsyncNotifierProvider` or `StreamNotifierProvider`. - **Unrestricted parameters.** A family takes any positional, named, optional or defaulted parameters instead of one argument. - **Auto-dispose by default**, with `@Riverpod(keepAlive: true)` as the opt-out. - **Stateful hot reload of providers**: when you edit a generated provider, hot reload re-executes that provider and only that provider. - **Debug metadata** the tooling picks up, and riverpod_lint rules that only work on generated providers, such as the `dependencies` checks. ## What it costs - A generator step and generated files that must be regenerated after every signature change. - An extra layer to read: the `_$ClassName` base classes and generated family classes. - No generated form of the legacy kinds: `StateProvider`, `StateNotifierProvider` and `ChangeNotifierProvider` live in `legacy.dart` in Riverpod 3 and are not generated; returning a `ChangeNotifier` needs `Raw<T>` and manual disposal. ## Side by side | Concern | Hand-written | `@riverpod` | |---|---|---| | Provider kind | chosen by you | derived from the signature | | Default lifetime | kept alive | auto-dispose | | Family parameters | one argument | any parameter list | | Build step | none | generator required | | Hot reload of an edited provider | not re-executed on its own | that provider re-executes | | Legacy StateNotifierProvider | available from `legacy.dart` | not generated | ## When hand-written fits 1. The project has no code generation and the team does not want to add it. 2. A small app or a package example where a few providers do not justify the tooling. 3. A gradual migration: hand-written and generated providers watch each other freely, so older providers can stay until rewritten. 4. Teaching or reviewing Riverpod's model, where `NotifierProvider<StepCounter, int>(StepCounter.new)` makes the types explicit. 5. Code that still depends on a legacy kind during a transition. ## Signals that tip the decision Beyond the documented rule, a few project facts usually settle it quickly: - **Existing generators.** If freezed or json_serializable already run on every change, the marginal cost of riverpod_generator is close to zero. - **Many parameterised providers.** Families with several inputs read far better as named parameters than as records, which favours codegen. - **Iteration speed on a large codebase.** When generation already takes noticeable time, adding more generated files adds to every rebuild. - **Team familiarity.** Developers new to Riverpod often find `@riverpod` easier because they never pick a provider class; developers who debug provider internals may prefer seeing the class spelled out. - **Lint coverage.** Several riverpod_lint rules, including the `dependencies` checks for scoped providers, work only on generated providers. ## How to answer in an interview State the documented rule first, then name two benefits and one cost, and finish with the scope of the decision: it is a per-project convention, not a per-provider one. Mixing both styles works technically, but a codebase that uses one style consistently is easier to review, to search and to hand over.

  • Can generated and hand-written providers be mixed in one Riverpod app?
    Yes. Both are ordinary Riverpod providers at runtime, so a `@riverpod` function can `ref.watch` a hand-written `Provider` and the reverse. That is what makes a file-by-file migration possible. The cost is readability: two declaration styles and two lifetime defaults in one codebase, so teams usually settle on one.
  • Why did Riverpod's advice on code generation soften?
    The stronger recommendation was tied to Dart macros, which would have generated code without a separate build step. Macros were cancelled, and build-time generation is still fairly slow, so the docs now say to use riverpod_generator mainly when the project already generates code for packages like freezed or json_serializable.

saying these in an interview costs you the question

  • Hand-written providers are deprecated in Riverpod 3, so codegen is mandatory.
  • Dart macros replaced riverpod_generator, so no build step is needed.
  • Generated providers are faster at runtime than hand-written ones.
  • riverpod_generator can generate StateNotifierProvider for older notifiers.
  • Only generated providers support overrides and AsyncValue.