Riverpod Skill
This skill defines how to correctly use Riverpod for state management in Flutter and Dart applications.
1. Setup
dartvoid main() { runApp(const ProviderScope(child: MyApp())); }
- Wrap your app with
ProviderScopedirectly inrunApp— never insideMyApp. - Install and use
riverpod_lintto enable IDE refactoring and enforce best practices.
2. Defining Providers
dart// Functional provider (codegen) @riverpod int example(Ref ref) => 0; // FutureProvider (codegen) @riverpod Future<List<Todo>> todos(Ref ref) async { return ref.watch(repositoryProvider).fetchTodos(); } // Notifier (codegen) @riverpod class TodosNotifier extends _$TodosNotifier { @override Future<List<Todo>> build() async { return ref.watch(repositoryProvider).fetchTodos(); } Future<void> addTodo(Todo todo) async { ... } }
- Define all providers as
finaltop-level variables. - Use
Provider,FutureProvider, orStreamProviderbased on the return type. - Use
ConsumerWidgetorConsumerStatefulWidgetinstead ofStatelessWidget/StatefulWidgetwhen accessing providers.
3. Using Ref
| Method | Use for |
|---|---|
ref.watch | Reactively listen — rebuilds when value changes. Use during build phase only. |
ref.read | One-time access — use in callbacks/Notifier methods, not in build. |
ref.listen | Imperative subscription — prefer ref.watch where possible. |
ref.onDispose | Cleanup when provider state is destroyed. |
dart// In a widget class MyWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final value = ref.watch(myProvider); return Text('$value'); } } // Cleanup in a provider final provider = StreamProvider<int>((ref) { final controller = StreamController<int>(); ref.onDispose(controller.close); return controller.stream; });
- Never call
ref.watchinside callbacks, listeners, or Notifier methods. - Use
ref.read(yourNotifierProvider.notifier).method()to call Notifier methods from the UI. - Check
context.mountedbefore usingrefafter anawaitin async callbacks.
4. Combining Providers
dart@riverpod Future<String> userGreeting(Ref ref) async { final user = await ref.watch(userProvider.future); return 'Hello, ${user.name}!'; }
- Use
ref.watch(asyncProvider.future)to await an async provider's resolved value. - Providers only execute once and cache the result — multiple widgets listening to the same provider share one computation.
5. Passing Arguments (Families)
dart@riverpod Future<Todo> todo(Ref ref, String id) async { return ref.watch(repositoryProvider).fetchTodo(id); } // Usage final todo = ref.watch(todoProvider('some-id'));
- Always enable
autoDisposefor parameterized providers to prevent memory leaks. - Use
Dart 3 recordsor code generation for multiple parameters — they naturally override==. - Avoid passing plain
ListorMapas parameters (no==override); useconstcollections, records, or classes with proper equality. - Use the
provider_parameterslint rule fromriverpod_lintto catch equality mistakes.
6. Auto Dispose & State Lifecycle
- With codegen: state is destroyed by default when no longer listened to. Opt out with
keepAlive: true. - Without codegen: state is kept alive by default. Use
.autoDisposeto enable disposal. - State is always destroyed when a provider is recomputed.
dart// keepAlive with timer ref.onCancel(() { final link = ref.keepAlive(); Timer(const Duration(minutes: 5), link.close); });
- Use
ref.onDisposefor cleanup; do not trigger side effects or modify providers inside it. - Use
ref.invalidate(provider)to force destruction; useref.invalidateSelf()from within the provider. - Use
ref.refresh(provider)to invalidate and immediately read the new value — always use the return value.
7. Eager Initialization
Providers are lazy by default. To eagerly initialize:
dart// In MyApp or a dedicated widget under ProviderScope: Consumer( builder: (context, ref, _) { ref.watch(myEagerProvider); // forces initialization return const MyApp(); }, )
- Place eager initialization in a public widget (not
main()) for consistent test behavior. - Use
AsyncValue.requireValueto read data directly and throw clearly if not ready.
8. Performing Side Effects
dart@riverpod class TodosNotifier extends _$TodosNotifier { Future<void> addTodo(Todo todo) async { state = const AsyncLoading(); state = await AsyncValue.guard(() async { await ref.read(repositoryProvider).addTodo(todo); return [...?state.value, todo]; }); } } // In UI: ElevatedButton( onPressed: () => ref.read(todosNotifierProvider.notifier).addTodo(todo), child: const Text('Add'), )
- Use
ref.read(notref.watch) in event handlers. - After a side effect, update state by: setting it directly, calling
ref.invalidateSelf(), or manually updating the cache. - Always handle loading and error states in the UI.
- Do not perform side effects in provider constructors or build methods.
9. Provider Observers
dartclass MyObserver extends ProviderObserver { @override void didUpdateProvider(ProviderObserverContext context, Object? previousValue, Object? newValue) { print('[${context.provider}] updated: $previousValue → $newValue'); } @override void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) { // Report to error service } } runApp(ProviderScope(observers: [MyObserver()], child: MyApp()));
10. Testing
dart// Unit test final container = ProviderContainer( overrides: [repositoryProvider.overrideWith((_) => FakeRepository())], ); addTearDown(container.dispose); expect(await container.read(todosProvider.future), isNotEmpty); // Widget test await tester.pumpWidget( ProviderScope( overrides: [repositoryProvider.overrideWith((_) => FakeRepository())], child: const MyApp(), ), );
- Create a new
ProviderContainerorProviderScopefor each test — never share state between tests. - Use
container.listenovercontainer.readforautoDisposeproviders to keep state alive during the test. - Use
overridesto inject mocks or fakes. - Prefer mocking dependencies (repositories) rather than Notifiers directly.
- If you must mock a Notifier, subclass the original — don't use
implementsorwith Mock. - Place Notifier mocks in the same file as the Notifier if using code generation.
- Obtain the container in widget tests with
ProviderScope.containerOf(tester.element(...)).


