mcpField
F
flutter-riverpod-expert

flutter-riverpod-expert

github.com/juparave开发11.1k 次安装5 天前更新
代码仓库

关于 flutter-riverpod-expert

面向 Flutter 开发者的 Riverpod 实践指导技能,涵盖 Provider 选择、代码生成、异步状态、性能、架构与测试。内容侧重文档所述的 2025 年实践,也提及 Riverpod 2.x 的迁移背景。

主要功能

  1. 01Provider 选择指导
  2. 02代码生成配置
  3. 03异步状态与变更操作
  4. 04使用 select() 优化重建
  5. 05Repository 与依赖模式
  6. 06Provider 和 Widget 测试

适用场景

  1. 01为状态需求选择 Provider
  2. 02组织异步数据与变更操作
  3. 03减少不必要的 Widget 重建
  4. 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)

  1. 强烈建议使用代码生成——使用 @riverpod 注解和 riverpod_generator
  2. 异步状态首选 AsyncNotifierProvider(可替代 FutureProvider/StreamProvider,保持一致性)
  3. 默认自动销毁——代码生成会让 providers 自动销毁
  4. 仓库模式——将数据层与状态管理分离
  5. 性能优先——使用 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 时:

  1. 始终推荐使用代码生成,并添加 @riverpod 注解
  2. 异步状态应优先使用 AsyncNotifierProvider,而不是 FutureProvider/StreamProvider
  3. 通过建议使用 select() 来监听特定字段,优化性能
  4. 遵循仓库模式,实现整洁架构
  5. 使用 AsyncValue.guard() 和 .when() 妥善处理错误
  6. 提醒用户注意 autoDispose 和缓存策略
  7. 如果发现用户代码中存在反模式,请指出
  8. 提供包含正确导入语句的完整可运行示例

参考

完整详情和高级模式,请参阅: /Users/pablito/EVOworkspace/flutter/CesarferPromotoresFlutter/promotores/RIVERPOD_2025_BEST_PRACTICES.md

评论

支持 Markdown