In Riverpod 3, when does riverpod_generator's @riverpod syntax pay off, and when do hand-written providers fit better?
answer
- codegen is optional in Riverpod
- already running freezed or json_serializable?
- macros were cancelled
- no provider-type choice, any parameters
- stateful hot reload of one provider
basics
~20 sRiverpod'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 sCode 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 linesimport '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
Know that @riverpod code generation is optional and that both styles give ordinary providers you read with ref.watch.
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.
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.
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.