Riverpod Best Practices (v3)
This skill outlines the standard operating procedures for using Riverpod v3 in this project. Adherence to these practices ensures maintainability, testability, and consistency.
Core Principles
- Code Generation (
@riverpod): ALWAYS use the@riverpodannotation andriverpod_generator. Do NOT define providers manually (e.g.,Provider((ref) =>...)).
- *Why:* Reduces boilerplate, handles autoDispose logic automatically, ensures type safety, and enables easier refactoring.
- Modular Architecture: Organize code by feature, not by layer.
- *Structure:* lib/modules/[feature]/providers/ for providers, lib/modules/[feature]/screens/ for UI.
- Unified
Ref: UseReffor all provider interactions. Avoid legacy types likeWidgetRefinside providers (useRefinstead). - AsyncValue First: Always handle Loading/Error/Data states explicitly in the UI using
switchexpressions.
Implementation Guidelines
1. Defining Providers
- Class-Based (Notifier): Use for complex state requiring methods (mutations).
@riverpod class MyNotifier extends _$MyNotifier {...} - Functional (Future/Stream): Use for simple data fetching or read-only values.
@riverpod Future<Data> myData(Ref ref) async {...} - KeepAlive: Use
@Riverpod(keepAlive: true)ONLY for global state (User, Auth, Settings). Default isautoDispose.
2. State Management
- Side Effects: Perform side effects (API calls, navigation logic) in methods within the Notifier, NOT in the
build()method. - Ref.mounted: Always check
ref.mountedafter anawaitbefore setting state to prevent exceptions on disposed providers.if (ref.mounted) state = AsyncData(data); - Mutations: To update lists/data, prefer fetching fresh data or optimistically updating state.
3. Consuming Providers
- ConsumerWidget: Extend
ConsumerWidgetinstead ofStatelessWidget. - ConsumerStatefulWidget: Extend
ConsumerStatefulWidgetif you needinitState/dispose. - Ref.watch: Use
ref.watchinsidebuild()to rebuild on changes. - Ref.read: Use
ref.readinside callbacks (e.g.,onPressed) to trigger actions. NEVER useref.readinsidebuild().
4. Naming Conventions
- File Name:
snake_case(e.g.,user_controller.dart). - Class Name:
PascalCase(e.g.,UserController). - Provider Name (Generated):
camelCase(e.g.,userControllerProvider).
Resources
- Code Snippets: See references/snippets.md for templates of Notifiers, Providers, and Consumers.
- Testing Guide: See references/testing.md for unit and widget testing patterns.
- Async Gaps & Lifecycle: See references/async_gaps.md for critical info on
UnmountedRefExceptionandkeepAlive. - Provider Types: See references/provider_types.md for a decision tree on which provider to use.
- Performance: See references/performance.md for
ref.selectand optimization tips. - Architecture: See references/architecture.md for repository patterns and anti-patterns.
Migration (from v2/Manual)
If you encounter manual providers (StateNotifierProvider, ChangeNotifierProvider, etc.):
- Identify: Locate the logic.
- Convert: Rewrite as a
@riverpodclass or function. - Replace: Update consumers to use the generated provider (e.g.,
myProviderinstead ofmyProvider.notifierfor watching state).