Flutter Riverpod Generator: Codegen Providers the Easy Way
Published: 2026-10-14 · 8 min read
The flutter riverpod generator replaces handwritten providers with one @riverpod annotation: setup, Notifier, AsyncNotifier, families, and testing explained.
If you learned Riverpod from older tutorials, your providers probably look like this: a Provider((ref) => ...) factory, a StateNotifierProvider with a create callback, generics stacked three deep just to say "this is an integer." It works, but it is boilerplate that exists only to satisfy the compiler. The flutter riverpod generator deletes most of that ceremony. You annotate a plain Dart function or class with @riverpod, run build_runner once, and get a fully typed, documented, auto-disposed provider without writing a single factory.
In this guide I will walk you through the whole thing the way I use it in production: setup in five minutes, plain function providers, notifiers, async data fetching, parameterized "family" providers, the right way to read state, and how to test generated providers. By the end you will never hand-write a provider again.
What the Flutter Riverpod Generator Actually Does
The generator (package riverpod_generator, a dev dependency) reads your source files, finds everything annotated with @riverpod, and emits a .g.dart part file containing the provider declarations. Your code stays declarative and readable; the generated code holds the plumbing.
The rules are simple:
- Annotate a function and you get a plain provider that recomputes its value.
- Annotate a class extending
_$Notifier(or_$AsyncNotifier) and you get a notifier with mutable state and methods. - The generated provider's name is your function/class name plus
Provider:greetingbecomesgreetingProvider. - Every generated provider is
autoDisposeby default — it cleans itself up when no widget listens anymore, so you stop leaking state between screens.
The one hard requirement: your file must declare part 'your_file.g.dart'; at the top, and you must run the code generator after every change. Miss either and the compiler errors are cryptic, so make this a habit from the start.
Setting It Up in Five Minutes
Add the runtime packages and the dev tooling:
dependencies:
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^3.0.0
riverpod_lint: ^3.0.0
riverpod_lint is optional but worth it — it catches mistakes like calling ref.watch outside a build method before they become runtime bugs. Then generate once:
dart run build_runner build --delete-conflicting-outputs
During development, keep a watcher running in a terminal so generated files update as you type:
dart run build_runner watch --delete-conflicting-outputs
--delete-conflicting-outputs matters more than it sounds. When you rename a provider or change its parameters, the old generated file conflicts with the new one; without the flag, the build fails and you get to debug a file you never wrote.
Wrap your app in a ProviderScope as usual — codegen changes nothing about the widget side:
void main() {
runApp(const ProviderScope(child: MyApp()));
}
Your First Generated Provider
Create lib/providers/greeting.dart:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'greeting.g.dart';
@riverpod
String greeting(GreetingRef ref) {
final hour = DateTime.now().hour;
if (hour < 12) return 'Good morning';
if (hour < 18) return 'Good afternoon';
return 'Good evening';
}
Run the generator and use it exactly like any provider:
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Text(greeting);
}
}
Compare that to the legacy syntax — Provider<String>((ref) { ... }) with the type written twice — and the win is obvious. One function, one annotation, zero generics.
Mutable State: Notifiers Without the Ceremony
For state you change over time, annotate a class. Here is a counter, the example every state-management tutorial is legally required to include:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
build() returns the initial state. Every method that assigns state triggers listeners. The generated counterProvider is an auto-disposing NotifierProvider — no StateNotifier, no create callback, no ref parameter threading.
In the UI you read and write through the notifier:
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('$count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Text('Increment'),
),
],
);
}
}
Notice the discipline: ref.watch inside build for reading (rebuilds the widget when state changes), ref.read inside the button callback for calling methods (no rebuild subscription). This one rule prevents a huge class of unnecessary rebuilds.
Async Data: AsyncNotifier
Network calls and database reads return futures. AsyncNotifier models the loading/error/data lifecycle for you:
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<User> build() async {
final userId = ref.watch(authStateProvider).userId;
return ref.watch(userRepositoryProvider).fetchUser(userId);
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => build());
}
}
The UI consumes it with .when, which forces you to handle all three states — no more forgetting the error branch:
final profile = ref.watch(userProfileProvider);
return profile.when(
data: (user) => ProfileCard(user: user),
loading: () => const CircularProgressIndicator(),
error: (err, _) => ErrorView(message: err.toString()),
);
Because the notifier watches authStateProvider, changing the logged-in user automatically refetches the profile. That reactive dependency graph is the whole point of Riverpod, and codegen makes it read like plain Dart.
keepAlive and Families, Done Simply
Two legacy pain points disappear with parameters on the annotation.
Long-lived providers. Generated providers auto-dispose by default, which is what you want for screen-scoped state. For app-scoped singletons — auth state, configuration, a cached repository — opt out:
@Riverpod(keepAlive: true)
AuthState authState(AuthStateRef ref) {
return AuthState.signedOut();
}
Families. The old syntax needed a separate Provider.family with awkward generics. With codegen, a function parameter just becomes a family:
@riverpod
Future<Product> product(ProductRef ref, String productId) {
return ref.watch(productRepositoryProvider).fetch(productId);
}
Consume it with the argument inline:
final product = ref.watch(productProvider('abc-123'));
Each distinct productId gets its own cached provider instance, independently auto-disposed. Passing two parameters works the same way — no tuple hacks.
Reading State the Right Way
Three methods, three jobs. Getting these mixed up is the most common Riverpod bug I see in code reviews:
| Method | Where | Purpose |
|---|---|---|
ref.watch |
Inside build (or another provider) |
Subscribe; rebuild when it changes |
ref.read |
Callbacks, initState, event handlers |
One-time access; no subscription |
ref.listen |
build or initState |
React to changes with side effects (navigation, snackbars) |
A practical ref.listen example — navigate after login without rebuilding anything:
ref.listen(authStateProvider, (previous, next) {
if (next.isSignedIn) {
context.go('/home');
}
});
And ref.invalidate is your "start over" button: ref.invalidate(userProfileProvider) disposes the provider and rebuilds it fresh, which retriggers build() and refetches data. Use it for pull-to-refresh and logout cleanup.
Testing Generated Providers
Generated providers test exactly like handwritten ones. Build a ProviderContainer, override dependencies, and assert:
test('user profile loads user data', () async {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(FakeUserRepository()),
],
);
addTearDown(container.dispose);
final user = await container.read(userProfileProvider.future);
expect(user.name, 'Test User');
});
overrideWithValue swaps the real repository for a fake; .future unwraps the AsyncValue so the test reads like a plain unit test. Because codegen gives every provider a concrete type, overrides are type-safe — the compiler rejects a fake that does not match.
Legacy Syntax vs Generator Syntax
| Task | Legacy syntax | Generator syntax |
|---|---|---|
| Simple value provider | final p = Provider<String>((ref) => 'x'); |
@riverpod String p(PRef ref) => 'x'; |
| Mutable state | StateNotifierProvider<Counter, int>((ref) => Counter()) |
@riverpod class Counter extends _$Counter |
| Async fetch | FutureProvider<User>((ref) async => ...) |
@riverpod class UserProfile extends _$UserProfile |
| Family | Provider.family<Product, String>((ref, id) => ...) |
@riverpod Future<Product> product(ProductRef ref, String id) |
| Keep alive | Provider(..., dependencies: ...) / manual |
@Riverpod(keepAlive: true) |
| Disposal | Manual autoDispose modifier |
Auto-dispose by default |
Every row is shorter on the right. Multiply that by the forty or fifty providers in a real app and codegen is not a convenience — it is a different codebase.
Pitfalls Checklist
- Forgot the
partdirective. Withoutpart 'file.g.dart';, the generator silently skips your file and the provider "doesn't exist." The error points at the usage site, not the cause. - Stale generated files after renames. Renaming a provider without
--delete-conflicting-outputsleaves the old.g.dartaround, and you get duplicate-declaration errors from code you didn't write. Delete the flag never. ref.watchin callbacks. Watching insideonPressedsubscribes a dead context and rebuilds nothing useful — or worse, throws. Callbacks getref.read.- keepAlive on everything. Opting out of auto-dispose for screen state leaks providers across navigation. Default (disposed) is correct for most providers; reserve
keepAlive: truefor genuine singletons. - Skipping
riverpod_lint. The linter flagsref.readinsidebuild, missingpartdirectives, and providers declared in the wrong place. It is free; enable it. - Watching a future with
.futureinbuild.ref.watch(provider.future)insidebuildrebuilds on every frame of the future — use.whenon theAsyncValueinstead.
Migration Plan: Legacy to Codegen in an Afternoon
- Add the three dev packages and generate once — nothing breaks, since you haven't changed any providers yet.
- Convert leaf providers first (simple values, repositories), then notifiers, then families. Each conversion is one annotation plus a regenerated file.
- Keep the old provider names as long as possible so call sites don't change; rename in a second pass.
- Delete legacy
StateNotifierclasses last, after their generated replacements are covered by the same tests.
Start with one feature, run the tests, and you will convert the rest of the app in an afternoon. The flutter riverpod generator turns provider authoring from a tax into a one-liner — which is exactly what a state-management library should do.
Need this wired into a real app? I build production Flutter apps with clean, codegen-driven state management every day — get in touch and let's talk about your project.
Frequently Asked Questions
Do I need build_runner to use Riverpod in Flutter?
Only if you use the flutter riverpod generator. Annotating providers with @riverpod requires running build_runner (or its watcher) to emit the .g.dart part files. The legacy handwritten provider syntax works without codegen, but you lose the shorter syntax, auto-dispose defaults, and type-safe families.
What does the @riverpod annotation generate?
A typed provider declaration in a .g.dart part file. A function becomes a plain provider, a class extending _$Notifier becomes a NotifierProvider, and a class extending _$AsyncNotifier becomes an async provider with loading, error, and data states.
How do I test a generated Riverpod provider?
Create a ProviderContainer with overrides for dependencies, such as userRepositoryProvider.overrideWithValue(FakeUserRepository()), then read the provider - or its .future for async providers - and assert the result. Generated providers are type-safe, so the compiler rejects mismatched fakes.
Should I use keepAlive on generated providers?
Only for genuine app-scoped singletons like auth state or configuration. Generated providers auto-dispose by default, which is correct for screen-scoped state; marking everything keepAlive leaks providers across navigation.