关于 flutter-riverpod-expert
面向 Flutter 开发者的 Riverpod 实践指导技能,涵盖 Provider 选择、代码生成、异步状态、性能、架构与测试。内容侧重文档所述的 2025 年实践,也提及 Riverpod 2.x 的迁移背景。
主要功能
- 01Provider 选择指导
- 02代码生成配置
- 03异步状态与变更操作
- 04使用 select() 优化重建
- 05Repository 与依赖模式
- 06Provider 和 Widget 测试
适用场景
- 01为状态需求选择 Provider
- 02组织异步数据与变更操作
- 03减少不必要的 Widget 重建
- 04测试依赖 Riverpod 的界面和 Provider
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ juparave/ dotfiles --skill flutter-riverpod-expert安装后在 Claude Code 中输入/flutter-riverpod-expert确认已加载。
详细介绍
Flutter Riverpod 专家指南——2025 年最佳实践
你精通符合 2025 年最佳实践的 Flutter Riverpod 状态管理。用户使用 Riverpod 或 Flutter 状态管理时,请遵循以下模式和指南。
何时使用此技能
用户提到以下内容时,启用这项专业知识:
- Riverpod、providers、状态管理或 StateNotifier
- AsyncNotifier、FutureProvider、StreamProvider、NotifierProvider
- 使用 riverpod_generator 或 build_runner 进行代码生成
- 数据获取、API 集成、变更或响应式状态
- 状态同步、缓存、autoDispose 或内存管理
- Provider 测试、依赖注入或仓库模式
- 重建、provider 选择或优化方面的性能问题
- 从旧版 Riverpod 模式迁移到现代方案
核心原则(2025)
- 强烈建议使用代码生成——使用
@riverpod注解和riverpod_generator - 异步状态首选 AsyncNotifierProvider(可替代 FutureProvider/StreamProvider,保持一致性)
- 默认自动销毁——代码生成会让 providers 自动销毁
- 仓库模式——将数据层与状态管理分离
- 性能优先——使用
select()优化重建
Provider 选择指南
快速决策树
不可变/计算值——使用 Provider:
@riverpod
String apiKey(Ref ref) => 'YOUR_API_KEY';
@riverpod
int totalPrice(Ref ref) {
final cart = ref.watch(cartProvider);
return cart.items.fold(0, (sum, item) => sum + item.price);
}
简单同步状态——使用 NotifierProvider:
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state = max(0, state - 1);
}
支持变更的异步数据(2025 年首选)——使用 AsyncNotifierProvider:
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async {
final repo = ref.watch(todoRepositoryProvider);
return repo.fetchTodos();
}
Future<void> addTodo(String title) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final repo = ref.read(todoRepositoryProvider);
await repo.createTodo(title);
return repo.fetchTodos();
});
}
Future<void> deleteTodo(String id) async {
// Optimistic update
state = AsyncData(state.value!.where((t) => t.id != id).toList());
try {
await ref.read(todoRepositoryProvider).deleteTodo(id);
} catch (e) {
ref.invalidateSelf(); // Rollback on error
}
}
}
仅限实时流——使用 StreamProvider:
@riverpod
Stream<User?> authState(Ref ref) {
return FirebaseAuth.instance.authStateChanges();
}
关键规则:优先使用 AsyncNotifierProvider,而不是 FutureProvider/StreamProvider,以获得更好的一致性和变更支持。
代码生成设置
依赖项(pubspec.yaml)
Riverpod 3.x(当前版本,截至 2025 年年中及之后):
dependencies:
flutter_riverpod: ^3.0.0
riverpod_annotation: ^4.0.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^4.0.0
riverpod_lint: ^3.0.0
注意:riverpod_lint 3.x 直接依赖 analyzer_plugin,而非独立的
custom_lint 包。不要将 custom_lint 添加到 pubspec 中——它无法与 riverpod_lint 3.1.8+ 一起解析。
请改为在 analysis_options.yaml 中注册插件:
analyzer:
plugins:
- riverpod_lint
Riverpod 2.x(旧版):
dependencies:
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^2.4.0
custom_lint: ^0.6.0
riverpod_lint: ^2.3.0
此处需要将 custom_lint 作为单独的依赖项,且 analysis_options.yaml 使用:
analyzer:
plugins:
- custom_lint
文件模板
每个 provider 文件都需要:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'filename.g.dart'; // REQUIRED
@riverpod
class MyProvider extends _$MyProvider {
@override
Future<Data> build() async => fetchData();
}
运行生成器
# Watch mode (RECOMMENDED during development)
dart run build_runner watch -d
# One-time generation
dart run build_runner build --delete-conflicting-outputs
性能优化模式
使用 ref.select() 选择特定字段
// ❌ BAD: Rebuilds on ANY product change
final product = ref.watch(productProvider);
return Text('\$${product.price}');
// ✅ GOOD: Only rebuilds when price changes
final price = ref.watch(productProvider.select((p) => p.price));
return Text('\$$price');
ref.watch() 与 ref.select()、ref.read() 和 ref.listen() 的区别
ref.watch()——订阅变化(在 build 中使用):
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return ListView(...);
}
ref.select()——订阅特定属性(优化重建):
final count = ref.watch(todoListProvider.select((todos) => todos.length));
final isAdult = ref.watch(personProvider.select((p) => p.age >= 18));
ref.read()——读取一次,不订阅(仅用于事件处理程序):
onPressed: () {
ref.read(todoListProvider.notifier).addTodo('New task');
}
// ❌ NEVER use read() in build to "optimize" - it won't rebuild!
ref.listen()——执行副作用(导航、提示条、日志记录):
ref.listen<AsyncValue<List<Todo>>>(
todoListProvider,
(previous, next) {
next.whenOrNull(
error: (error, stack) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $error')),
);
},
);
},
);
避免在循环中监听
// ❌ BAD: Causes performance issues
ListView.builder(
itemBuilder: (context, index) {
final todo = ref.watch(todoProvider(ids[index])); // DON'T!
return ListTile(...);
},
);
// ✅ GOOD: Separate widget for each item
class TodoItem extends ConsumerWidget {
const TodoItem({required this.todoId});
final String todoId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final todo = ref.watch(todoProvider(todoId));
return ListTile(title: Text(todo.title));
}
}
class TodoList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final ids = ref.watch(todoIdsProvider);
return ListView.builder(
itemCount: ids.length,
itemBuilder: (context, index) => TodoItem(todoId: ids[index]),
);
}
}
创建派生 providers
// ✅ GOOD: Separate provider for computed state
@riverpod
List<Todo> filteredSortedTodos(Ref ref) {
final todos = ref.watch(todoListProvider);
final filter = ref.watch(filterProvider);
final sortOrder = ref.watch(sortOrderProvider);
final filtered = todos.where((t) => t.matches(filter)).toList();
return filtered..sort(sortOrder.comparator);
}
仓库模式架构
三层架构
1. 数据层——仓库:
@riverpod
TodoRepository todoRepository(Ref ref) {
return TodoRepository(dio: ref.watch(dioProvider));
}
class TodoRepository {
TodoRepository({required this.dio});
final Dio dio;
Future<List<Todo>> fetchTodos() async {
final response = await dio.get('/todos');
return (response.data as List)
.map((json) => Todo.fromJson(json))
.toList();
}
Future<Todo> createTodo(String title) async {
final response = await dio.post('/todos', data: {'title': title});
return Todo.fromJson(response.data);
}
Future<void> deleteTodo(String id) async {
await dio.delete('/todos/$id');
}
}
2. 应用层——状态管理:
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async {
final repository = ref.watch(todoRepositoryProvider);
return repository.fetchTodos();
}
Future<void> addTodo(String title) async {
final repository = ref.read(todoRepositoryProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await repository.createTodo(title);
return repository.fetchTodos();
});
}
}
3. 表现层——UI:
class TodoListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todosAsync = ref.watch(todoListProvider);
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: todosAsync.when(
data: (todos) => ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => TodoTile(todos[index]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorView(error: error),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context, ref),
child: const Icon(Icons.add),
),
);
}
}
依赖注入
// Services
@riverpod
Dio dio(Ref ref) {
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(LogInterceptor());
return dio;
}
@riverpod
Future<SharedPreferences> sharedPreferences(Ref ref) async {
return await SharedPreferences.getInstance();
}
@riverpod
AuthService authService(Ref ref) {
return AuthService(
dio: ref.watch(dioProvider),
storage: ref.watch(sharedPreferencesProvider).value!,
);
}
// Repositories depend on services
@riverpod
UserRepository userRepository(Ref ref) {
return UserRepository(
dio: ref.watch(dioProvider),
authService: ref.watch(authServiceProvider),
);
}
// State providers depend on repositories
@riverpod
class CurrentUser extends _$CurrentUser {
@override
Future<User?> build() async {
final authService = ref.watch(authServiceProvider);
final userId = await authService.getCurrentUserId();
if (userId == null) return null;
final repository = ref.watch(userRepositoryProvider);
return repository.fetchUser(userId);
}
Future<void> logout() async {
final authService = ref.read(authServiceProvider);
await authService.logout();
ref.invalidateSelf();
}
}
Family Providers(参数化)
添加参数后,代码生成会自动创建 family providers:
// Simple family provider
@riverpod
Future<User> user(Ref ref, String id) async {
final dio = ref.watch(dioProvider);
final response = await dio.get('/users/$id');
return User.fromJson(response.data);
}
// Usage
final user = ref.watch(userProvider('123'));
// Family with AsyncNotifier
@riverpod
class UserNotifier extends _$UserNotifier {
@override
Future<User> build(String id) async {
final repo = ref.watch(userRepositoryProvider);
return repo.fetchUser(id);
}
Future<void> updateName(String newName) async {
final userId = arg; // Access the parameter via 'arg'
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(userRepositoryProvider).updateUser(userId, name: newName);
return ref.read(userRepositoryProvider).fetchUser(userId);
});
}
}
// Complex parameters need proper equality
class UserFilter {
const UserFilter({required this.role, required this.active});
final String role;
final bool active;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserFilter &&
role == other.role &&
active == other.active;
@override
int get hashCode => Object.hash(role, active);
}
@riverpod
Future<List<User>> filteredUsers(Ref ref, UserFilter filter) async {
return fetchUsers(filter);
}
AutoDispose 与缓存
代码生成会让 providers 默认自动销毁。
// Default: auto-dispose when no listeners
@riverpod
Future<String> data(Ref ref) async => fetchData();
// Keep alive permanently
@Riverpod(keepAlive: true)
Future<Config> config(Ref ref) async => loadConfig();
// Conditional keep alive - cache on success
@riverpod
Future<String> cachedData(Ref ref) async {
final data = await fetchData();
ref.keepAlive(); // Cache this result forever
return data;
}
// Timed cache (5 minutes)
@riverpod
Future<String> timedCache(Ref ref) async {
final data = await fetchData();
final link = ref.keepAlive();
Timer(const Duration(minutes: 5), link.close);
return data;
}
// Manual disposal - cleanup resources
@riverpod
Stream<int> websocket(Ref ref) {
final client = WebSocketClient();
ref.onDispose(() {
client.close(); // Cleanup when provider is disposed
});
return client.stream;
}
错误处理
全面错误处理
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async {
try {
final repository = ref.watch(todoRepositoryProvider);
return await repository.fetchTodos();
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
ref.read(authServiceProvider).logout();
throw UnauthorizedException();
}
throw NetworkException(e.message);
} catch (e) {
throw UnexpectedException(e.toString());
}
}
Future<void> addTodo(String title) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final repository = ref.read(todoRepositoryProvider);
await repository.createTodo(title);
return repository.fetchTodos();
});
}
}
UI 错误处理
// Using .when()
todosAsync.when(
data: (todos) => ListView.builder(...),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) {
if (error is NetworkException) {
return ErrorView(
message: 'Network error. Check your connection.',
onRetry: () => ref.invalidate(todoListProvider),
);
}
if (error is UnauthorizedException) {
return const ErrorView(message: 'Please log in again.');
}
return ErrorView(message: 'Error: $error');
},
);
// Listen for errors (side effects)
ref.listen<AsyncValue<List<Todo>>>(
todoListProvider,
(previous, next) {
next.whenOrNull(
error: (error, stack) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.toString()),
backgroundColor: Colors.red,
),
);
},
);
},
);
测试
Provider 测试
test('TodoList fetches todos correctly', () async {
final container = ProviderContainer.test(
overrides: [
todoRepositoryProvider.overrideWithValue(MockTodoRepository()),
],
);
final todos = await container.read(todoListProvider.future);
expect(todos.length, 2);
expect(todos[0].title, 'Test Todo 1');
});
test('TodoList adds todo correctly', () async {
final mockRepo = MockTodoRepository();
final container = ProviderContainer.test(
overrides: [
todoRepositoryProvider.overrideWithValue(mockRepo),
],
);
await container.read(todoListProvider.notifier).addTodo('New Todo');
verify(() => mockRepo.createTodo('New Todo')).called(1);
});
Widget 测试
testWidgets('TodoListScreen displays todos', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
todoRepositoryProvider.overrideWithValue(MockTodoRepository()),
],
child: const MaterialApp(home: TodoListScreen()),
),
);
await tester.pumpAndSettle();
expect(find.text('Test Todo 1'), findsOneWidget);
expect(find.text('Test Todo 2'), findsOneWidget);
});
应避免的常见反模式
性能陷阱
// ❌ Using ref.read() to avoid rebuilds
final todos = ref.read(todoListProvider); // Won't rebuild!
// ✅ Use ref.watch() or ref.select()
final count = ref.watch(todoListProvider.select((todos) => todos.length));
内存泄漏
// ❌ Not disposing resources
@riverpod
Stream<int> badWebsocket(Ref ref) {
final client = WebSocketClient();
return client.stream; // Never closed!
}
// ✅ Dispose resources
@riverpod
Stream<int> goodWebsocket(Ref ref) {
final client = WebSocketClient();
ref.onDispose(() => client.close());
return client.stream;
}
多个事实来源
// ❌ BAD: Which is the source of truth?
class BadWidget extends StatefulWidget {
int localCount = 0; // Local state
@override
Widget build(BuildContext context, WidgetRef ref) {
final providerCount = ref.watch(counterProvider); // Provider state
return Text('$localCount vs $providerCount'); // Confusing!
}
}
// ✅ GOOD: Single source of truth
class GoodWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
未使依赖的 providers 失效
// ❌ BAD
Future<void> logout() async {
state = null;
// Other providers still have old user data!
}
// ✅ GOOD: Invalidate dependent providers
Future<void> logout() async {
state = null;
ref.invalidate(userProfileProvider);
ref.invalidate(userSettingsProvider);
ref.invalidate(userNotificationsProvider);
}
// ✅ EVEN BETTER: Make providers watch auth
@riverpod
Future<UserProfile> userProfile(Ref ref) async {
final user = ref.watch(authProvider);
if (user == null) throw UnauthenticatedException();
return fetchUserProfile(user.id); // Auto-refetches when user changes
}
使用说明
当用户使用 Riverpod 时:
- 始终推荐使用代码生成,并添加
@riverpod注解 - 异步状态应优先使用 AsyncNotifierProvider,而不是 FutureProvider/StreamProvider
- 通过建议使用
select()来监听特定字段,优化性能 - 遵循仓库模式,实现整洁架构
- 使用
AsyncValue.guard()和.when()妥善处理错误 - 提醒用户注意 autoDispose 和缓存策略
- 如果发现用户代码中存在反模式,请指出
- 提供包含正确导入语句的完整可运行示例
参考
完整详情和高级模式,请参阅:
/Users/pablito/EVOworkspace/flutter/CesarferPromotoresFlutter/promotores/RIVERPOD_2025_BEST_PRACTICES.md