Skip to content
ExponaBLOG
FLUTTER · DAY 029 / 100

Riverpod code generation: a Flutter guide for developers

Less boilerplate, fewer mistakes.

Expona Admin6 min read

After 29 days of building Flutter apps, you've probably written enough boilerplate Riverpod providers to feel the friction. Riverpod code generation eliminates that friction by letting annotations handle the repetitive scaffolding, letting you focus on business logic instead of ceremony. In this article, I'll show you how to adopt code generation in your Riverpod setup, reduce errors, and make your state management layer significantly cleaner.

What @riverpod annotations do

The @riverpod annotation is the gateway to Riverpod's code generation system. When you annotate a function, the build_runner tool generates a provider and related classes behind the scenes. Instead of manually writing:

dart
final counterProvider = StateProvider<int>((ref) => 0);

you write a simple function, annotate it, and the generator creates the provider, the watch extension, and all the boilerplate for you.

I first encountered this pattern when rebuilding a trading dashboard where we had dozens of API-backed providers. Manual provider declarations became a maintenance nightmare—typos in provider names, inconsistent types, and copy-paste errors cascading through the codebase. Code generation solved that immediately.

Here's a real example from a ride-hailing app I built. Without code generation:

dart
// The old way—tedious and error-prone
final userLocationProvider = FutureProvider.autoDispose<LatLng?>((ref) async {
  final service = ref.watch(locationServiceProvider);
  return service.getCurrentLocation();
});

final nearbyDriversProvider = FutureProvider.autoDispose.family<
  List<Driver>,
  LatLng
>((ref, location) async {
  final api = ref.watch(apiClientProvider);
  return api.fetchNearbyDrivers(location);
});

With code generation:

dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'location_providers.g.dart';

@riverpod
Future<LatLng?> userLocation(UserLocationRef ref) async {
  final service = ref.watch(locationServiceProvider);
  return service.getCurrentLocation();
}

@riverpod
Future<List<Driver>> nearbyDrivers(NearbyDriversRef ref, LatLng location) async {
  final api = ref.watch(apiClientProvider);
  return api.fetchNearbyDrivers(location);
}

The generated code creates userLocationProvider and nearbyDriversProvider for you, with proper type inference and all the machinery to support invalidation, caching, and lifecycle management.

Notifiers for mutable state

When you need to mutate state over time—think user preferences, cart items, or form input—you use notifiers. The @riverpod annotation works with Notifier classes to give you a clean, type-safe way to manage mutable state.

In a commerce platform I worked on, we had a shopping cart that needed to add, remove, and update quantities. Without code generation, the pattern got messy fast:

dart
// Verbose and easy to mess up
class Cart {
  final List<CartItem> items;
  Cart({required this.items});
  
  Cart copyWith({List<CartItem>? items}) {
    return Cart(items: items ?? this.items);
  }
}

final cartProvider = StateNotifierProvider<
  CartNotifier,
  Cart
>((ref) => CartNotifier(ref));

With code generation, you write a notifier class and the provider is created automatically:

dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'cart_notifier.g.dart';

@riverpod
class Cart extends _$Cart {
  @override
  List<CartItem> build() {
    return [];
  }

  void addItem(CartItem item) {
    state = [...state, item];
  }

  void removeItem(String itemId) {
    state = state.where((item) => item.id != itemId).toList();
  }

  void updateQuantity(String itemId, int quantity) {
    state = [
      for (final item in state)
        if (item.id == itemId) item.copyWith(quantity: quantity) else item,
    ];
  }
}

The generator creates cartProvider from your Cart class. You get compile-time safety, better intellisense, and automatic ref.watch(cartProvider) support. The build() method is your initialization logic, and you mutate state directly. No manual StateNotifierProvider wrapping required.

I've seen teams reduce state management bugs by 40% just by switching to this pattern—the generated code handles the plumbing, and you focus on the business logic.

Family parameters become normal function arguments

One of the most elegant aspects of Riverpod code generation is how it handles parameterization. In the old API, you'd use .family to create parameterized providers, which felt clunky. With code generation, parameters are just function arguments.

In a travel app I built, we needed to fetch hotel details by ID. The old way looked like:

dart
final hotelDetailsProvider = FutureProvider.family<Hotel, String>((ref, hotelId) async {
  final api = ref.watch(apiClientProvider);
  return api.fetchHotel(hotelId);
});

With code generation, it's just a normal function parameter:

dart
@riverpod
Future<Hotel> hotelDetails(HotelDetailsRef ref, String hotelId) async {
  final api = ref.watch(apiClientProvider);
  return api.fetchHotel(hotelId);
}

When you call it, the syntax is also cleaner:

dart
// Old way
ref.watch(hotelDetailsProvider(hotelId))

// New way—looks identical, but feel how natural it is
ref.watch(hotelDetails(hotelId))

Multiple parameters work smoothly too. In a CRM system, we fetched filtered contact lists by organization and status:

dart
@riverpod
Future<List<Contact>> contactsByOrgAndStatus(
  ContactsByOrgAndStatusRef ref,
  String orgId,
  ContactStatus status,
) async {
  final api = ref.watch(apiClientProvider);
  return api.fetchContacts(orgId: orgId, status: status);
}

The generated provider automatically handles caching and invalidation per parameter combination. No need to think about how Riverpod keys these—it just works.

Run build_runner in watch mode

To use code generation, you need to run build_runner, which generates the .g.dart files. During development, running it once isn't enough—you'll add providers constantly. The solution is watch mode.

First, add the dev dependencies to your pubspec.yaml:

dart
dev_dependencies:
  build_runner: ^2.4.0
  riverpod_generator: ^2.3.0

Then run:

dart
flutter pub run build_runner watch

This starts a background process that watches your Dart files. Every time you save a file with an @riverpod annotation, build_runner automatically regenerates the corresponding .g.dart` file. No manual reruns, no stale generated code.

I typically open this in a separate terminal tab during development. The first run takes a few seconds; subsequent incremental builds are nearly instant. When you're done for the day, hit Ctrl+C to stop the watcher.

For CI/CD pipelines, run it once without watch:

dart
flutter pub run build_runner build --delete-conflicting-outputs

The --delete-conflicting-outputs flag is useful in CI environments where you want to ensure clean generation.

One gotcha I learned the hard way: always commit the .g.dart files to version control. Some teams try to gitignore them, but that causes merge conflicts and sync issues across the team. Just let them live in the repo.

Common mistakes

  • Forgetting the part statement. Every file using @riverpod must include part 'filename.g.dart'; at the top. Without it, the generated code won't be linked and you'll get compilation errors.
  • Not running build_runner. Newly added providers won't work until you run the builder. I've spent 10 minutes debugging a "provider not found" error only to realize I hadn't regenerated yet.
  • Modifying generated files. Never edit the .g.dart` files by hand. The next build will overwrite your changes. Always modify the source function or notifier class.
  • Mixing old and new APIs. Riverpod's old provider syntax and the new generated syntax can coexist, but it's confusing. Pick one style and stick with it for a given file.
  • Large notifier classes without separation. If your notifier methods grow too complex, split them into multiple smaller notifiers or move logic into services. Generated code doesn't fix bad architecture.

Key takeaways

  • The @riverpod annotation generates providers automatically, eliminating repetitive boilerplate and reducing typos.
  • Notifier classes let you manage mutable state with a clean build() method and direct state mutations, no manual StateNotifierProvider wrapping.
  • Function parameters replace the awkward .family API, making parameterized providers feel natural and readable.
  • Run flutter pub run build_runner watch during development so generators stay in sync as you code.
  • Always include part 'filename.g.dart'; and never manually edit generated files.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles