riverpod
$
npx mdskill add evanca/flutter-ai-rules/riverpodManage Flutter state with Riverpod providers and reactive patterns.
- Enforces provider setup, disposal, and side effect handling.
- Integrates with Dart, Flutter, and riverpod_lint tools.
- Decides recommendations by analyzing state management needs.
- Delivers results through reactive UI updates and provider watches.
SKILL.md
.github/skills/riverpodView on GitHub ↗
---
name: riverpod
description: Uses Riverpod for state management in Flutter/Dart. Use when setting up providers, combining requests, managing state disposal, passing arguments, performing side effects, testing providers, or applying Riverpod best practices.
---
# Riverpod Skill
This skill defines how to correctly use Riverpod for state management in Flutter and Dart applications.
---
## 1. Setup
```dart
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
```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 **`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. |
```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.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
```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 `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.
```dart
// 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:
```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.requireValue` to 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` (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
```dart
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
```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** `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
- [Riverpod GitHub Repository](https://github.com/rrousselGit/riverpod)
More from evanca/flutter-ai-rules
- architecture-feature-firstStructures Flutter apps using layered architecture (UI / Logic / Data) with feature-first file organization. Use when creating new features, designing the project structure, adding repositories/services/view models (or cubits/providers/notifiers), or wiring dependency injection. State management agnostic.
- blocImplements Flutter state management using the bloc library (Bloc and Cubit). Use when creating new features, screens, or state management logic with bloc/cubit, modeling state, wiring Flutter widgets to blocs, or writing bloc/cubit unit tests.
- dart-3-updatesApplies Dart 3 language features in Flutter/Dart code. Use when writing if-else or switch statements, creating new classes, or deciding between a data class and a record.
- effective-dartApplies Effective Dart guidelines in Flutter/Dart code. Use when writing or reviewing Dart code for naming conventions, types, style, imports, file structure, usage patterns, documentation, testing, widgets, state management, or performance.
- firebase-aiIntegrates Firebase AI Logic into Flutter apps. Use when setting up the firebase_ai plugin, calling Gemini models, handling AI service errors, or applying security and privacy considerations for AI features.
- firebase-analyticsIntegrates Firebase Analytics into Flutter apps. Use when setting up analytics, logging events, setting user properties, or configuring event parameters.
- firebase-app-checkIntegrates Firebase App Check into Flutter apps. Use when setting up App Check, selecting providers per platform, using debug providers during development, enabling enforcement, or applying App Check security best practices.
- firebase-authIntegrates Firebase Authentication into Flutter apps. Use when setting up auth, managing auth state, implementing email/password or social sign-in, handling auth errors, managing users, or applying security best practices.
- firebase-cloud-firestoreIntegrates Cloud Firestore into Flutter apps. Use when setting up Firestore, designing document/collection structure, reading and writing data, working with real-time listeners, designing for scale, or applying security rules.
- firebase-cloud-functionsCalls Firebase Cloud Functions from Flutter apps. Use when setting up callable functions, passing data to functions, handling errors from function calls, optimizing performance, or testing with the Firebase Emulator Suite.