| name | riverpod |
| description | Use when setting up providers, combining requests, managing state disposal, passing arguments, performing side effects, or testing providers (Riverpod). |
Riverpod Skill
This skill defines how to correctly use Riverpod for state management in Flutter and Dart applications.
1. Setup
void main() {
runApp(const ProviderScope(child: MyApp()));
}
- Wrap your app with
ProviderScope directly in runApp — never inside MyApp.
- Install and use
riverpod_lint to enable IDE refactoring and enforce best practices.
2. Defining Providers
// 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
final top-level variables.
- Use
Provider, FutureProvider, or StreamProvider based on the return type.
- Use
ConsumerWidget or ConsumerStatefulWidget instead of StatelessWidget/StatefulWidget when 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. |
// 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.watch inside callbacks, listeners, or Notifier methods.
- Use
ref.read(yourNotifierProvider.notifier).method() to call Notifier methods from the UI.
- Check
context.mounted before using ref after an await in async callbacks.
4. Combining Providers
@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)
@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
autoDispose for parameterized providers to prevent memory leaks.
- Use
Dart 3 records or code generation for multiple parameters — they naturally override ==.
- Avoid passing plain
List or Map as parameters (no == override); use const collections, records, or classes with proper equality.
- Use the
provider_parameters lint rule from riverpod_lint to 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
.autoDispose to enable disposal.
- State is always destroyed when a provider is recomputed.
// keepAlive with timer
ref.onCancel(() {
final link = ref.keepAlive();
Timer(const Duration(minutes: 5), link.close);
});
- Use
ref.onDispose for cleanup; do not trigger side effects or modify providers inside it.
- Use
ref.invalidate(provider) to force destruction; use ref.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:
// 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.requireValue to read data directly and throw clearly if not ready.
8. Performing Side Effects
@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 (not ref.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
class 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
// 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
ProviderContainer or ProviderScope for each test — never share state between tests.
- Use
container.listen over container.read for autoDispose providers to keep state alive during the test.
- Use
overrides to inject mocks or fakes.
- Prefer mocking dependencies (repositories) rather than Notifiers directly.
- If you must mock a Notifier, subclass the original — don't use
implements or with 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(...)).
References