Skip to content

· 6 min read · Nadeera Silva

Mobile

Riverpod 3 Best Practices for Production Flutter Apps (2026)

Riverpod 3 best practices for production Flutter apps: migrating from 2.x and StateProvider, structure, the generator, testing, and how it compares with Bloc.

Four stacked translucent teal glass panes linked by amber threads, with a smartphone resting on the top pane, illustrating layered Riverpod providers feeding the UI

Riverpod is still the state management library I reach for first in new Flutter apps. Riverpod 3.0 shipped in September 2025, and the current release is flutter_riverpod 3.4.3. A lot of what's online is written for 2.x, so this post covers the Riverpod 3 best practices I use in production: how to structure providers, how to migrate from StateProvider and StateNotifier, whether you need the generator, how to test, and how Riverpod compares with Bloc.

What's new in Riverpod 3

The Riverpod team warns that 3.0 "contains a few life-cycle changes" that "could break your app in subtle ways". These are the ones to know before upgrading:

Change in 3.0What it means for you
Automatic retryA provider that throws during initialization retries with exponential backoff. Configure or disable it with retry on ProviderScope or per provider.
Legacy providers movedStateProvider, StateNotifierProvider and ChangeNotifierProvider now live in legacy.dart. They work, but new code should use Notifier and AsyncNotifier.
One Ref typeAutoDisposeNotifier, FamilyNotifier and the typed Ref variants are gone.
Ref.mountedCheck it after an await before touching state, like BuildContext.mounted.
Updates filtered with ==All providers use == to decide whether to notify listeners.
Out-of-view listeners pauseWidgets that aren't visible stop receiving updates until they're shown again.
ProviderContainer.testA test container that disposes itself when the test ends.

Offline persistence and mutations also arrived in 3.0, but the docs mark both as experimental. I wouldn't build a production feature on either yet.

Riverpod 3 migration from 2.x, step by step

  1. Bump flutter_riverpod to 3.x (and riverpod_annotation, riverpod_generator and riverpod_lint if you use them).
  2. Do a case-sensitive replace of AutoDispose with nothing, as the migration guide suggests: AutoDisposeNotifier becomes Notifier.
  3. Change legacy imports to package:flutter_riverpod/legacy.dart so the app compiles, then migrate those providers one feature at a time.
  4. Move family arguments from build into a constructor (example below).
  5. Add if (!ref.mounted) return; after awaits in notifier methods.
  6. Decide on retry: keep the default, or turn it off where you show your own error UI.

Replacing StateProvider with a Notifier

// Before: StateProvider, now in legacy.dart
final counterProvider = StateProvider<int>((ref) => 0);
ref.read(counterProvider.notifier).state++;

// After: a Notifier with named methods
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

class Counter extends Notifier<int> {
  @override
  int build() => 0;

  void increment() => state++;
}

ref.read(counterProvider.notifier).increment();

It's a few more lines, but every change now goes through a method you can search for, log and test.

Family notifiers take a constructor argument

final todoProvider =
    AsyncNotifierProvider.autoDispose.family<TodoNotifier, Todo, String>(TodoNotifier.new);

class TodoNotifier extends AsyncNotifier<Todo> {
  TodoNotifier(this.id);
  final String id;

  @override
  Future<Todo> build() => ref.watch(todoRepositoryProvider).fetch(id);
}

A family caches one provider per argument using == and hashCode. Pass IDs or records, not a new list or an object without value equality, or you'll get a fresh provider on every rebuild.

How I structure providers in a production app

Diagram of three layers: a repository provider wrapping the API, an AsyncNotifier per feature holding state, and widgets that watch state and call notifier methods
Data flows up through watch. Changes flow down through notifier methods.

Three layers, each in its own feature folder:

  • A repository provider that wraps the API or database. No UI state.
  • An AsyncNotifier per feature that loads in build and exposes methods for changes.
  • Widgets that watch the notifier and call its methods from event handlers.
import 'package:flutter_riverpod/flutter_riverpod.dart';

final todoRepositoryProvider = Provider<TodoRepository>((ref) {
  return HttpTodoRepository(ref.watch(httpClientProvider));
});

final todosProvider =
    AsyncNotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);

class TodosNotifier extends AsyncNotifier<List<Todo>> {
  @override
  Future<List<Todo>> build() => ref.watch(todoRepositoryProvider).fetchAll();

  Future<void> add(String title) async {
    await ref.read(todoRepositoryProvider).add(title);
    if (!ref.mounted) return; // the provider may have been disposed meanwhile
    ref.invalidateSelf();
    await future;
  }
}

build only reads. Side effects are methods the UI calls on purpose, so a rebuild can't send a request twice.

In widgets, watch in build and read in callbacks. AsyncValue is sealed, so a switch forces you to handle every state:

class TodoList extends ConsumerWidget {
  const TodoList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return switch (ref.watch(todosProvider)) {
      AsyncData(:final value) => ListView(
          children: [for (final t in value) Text(t.title)],
        ),
      AsyncError(:final error) => Center(child: Text('Could not load: $error')),
      _ => const Center(child: CircularProgressIndicator()),
    };
  }
}

Do you need riverpod_generator?

No. Riverpod's docs say code generation "is completely optional", and every example above works without build_runner. I use it on bigger projects because families and auto-dispose become plain function parameters:

import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'todo.g.dart';

@riverpod
Future<Todo> todo(Ref ref, String id) =>
    ref.watch(todoRepositoryProvider).fetch(id);
// generates todoProvider, used as ref.watch(todoProvider('42'))

Run dart run build_runner watch while you work. Generated providers are auto-dispose by default; add @Riverpod(keepAlive: true) where you want them cached.

Testing Riverpod providers

import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';

void main() {
  test('loads todos from the repository', () async {
    final container = ProviderContainer.test(
      overrides: [
        todoRepositoryProvider.overrideWithValue(FakeTodoRepository()),
      ],
    );

    final todos = await container.read(todosProvider.future);
    expect(todos, hasLength(2));
  });
}

Awaiting .future waits for the async state to settle, with no timers or pump calls. For widget tests, wrap the widget in a ProviderScope with the same overrides.

Riverpod vs Bloc

I've shipped both. This is how I'd compare them for a team choosing today:

Riverpod 3Bloc (flutter_bloc 9)
Unit of stateProviders and notifiersBlocs (events in, states out) and Cubits
Dependency injectionBuilt in: providers read other providersThrough BlocProvider and RepositoryProvider in the widget tree
BoilerplateLow, lower with the generatorHigher for Blocs, similar for Cubits
Async loading and errorsAsyncValue built inYou model loading and error states yourself
Works outside widgetsYes, ProviderContainerYes, Blocs are plain Dart

Bloc's event-driven style suits teams that want every change as a named event with a strict log. For most apps I build, Riverpod gets to the same structure with less code. If an app already uses Bloc well, I don't migrate it.

Riverpod best practices checklist

  • New code uses Notifier and AsyncNotifier, not legacy providers.
  • build reads, methods write.
  • ref.mounted after every await in a notifier.
  • Family arguments are IDs or records with value equality.
  • autoDispose on detail screens opened many times.
  • riverpod_lint enabled. Since 3.x it runs as an analysis server plugin: add plugins: riverpod_lint: <version> to analysis_options.yaml, and dart analyze reports it in CI.
  • The Riverpod DevTools extension (updated in 3.4.1) for inspecting providers.

Inheriting an app with tangled state? My playbook for taking over a live Flutter app covers stabilising it first. Upgrading the iOS side too? See the Flutter UIScene migration guide.

Frequently asked questions

What's new in Riverpod 3?

Automatic retry for failing providers, legacy providers moved to legacy.dart, one Ref type, Ref.mounted, == filtering for all providers, paused out-of-view listeners and ProviderContainer.test.

How do I migrate from Riverpod 2 to 3?

Remove AutoDispose from notifier class names, change legacy imports to legacy.dart, move family arguments into the notifier constructor, add ref.mounted checks after awaits, and decide how you want retry to behave.

Are StateNotifierProvider and StateProvider removed in Riverpod 3?

No. They moved to the legacy import, for example package:flutter_riverpod/legacy.dart. New code should use Notifier and AsyncNotifier.

Is riverpod_generator required?

No. Riverpod's docs say code generation is completely optional. It reduces boilerplate for families and auto-dispose providers.

Riverpod vs Bloc: which should I use?

Both are solid. Bloc suits teams that want every change as a named event. Riverpod reaches a similar structure with less code and has async loading and error states built in. Don't migrate an app that already uses Bloc well.

Why does my failing provider keep retrying?

Riverpod 3 retries providers that fail during initialization, with exponential backoff. Pass a retry function that returns null to turn it off.

How do I test an AsyncNotifier?

Create a container with ProviderContainer.test, override the repository with a fake, and await container.read(provider.future).

Sources

  1. What's new in Riverpod 3.0 (Riverpod)
  2. Migrating from 2.0 to 3.0 (Riverpod)
  3. About code generation (Riverpod)
  4. Testing your providers (Riverpod)
  5. flutter_riverpod (pub.dev)
  6. riverpod changelog (pub.dev)
  7. riverpod_generator (pub.dev)
  8. riverpod_lint (pub.dev)
  9. flutter_bloc (pub.dev)
SharePost on XLinkedIn

Comments

Share a question or note about this article.

Loading comments…