移动/游戏
Flutter/Dart 项目用 Riverpod…
适用于 Flutter/Dart 应用开发:需要 Riverpod 状态管理、Freezed 不可变模型、go_router 导航、mocktail 测试,并希望统一项目结构、依赖、lint 与 CI 配置。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
Flutter/Dart 项目用 Riv特定
解决思路
- 项目结构:在 lib/ 下按 core(constants、extensions、router/app_router.dart、theme/app_theme.dart)、data(models Freezed 模型、repositories 实现、services API)、domain(entities、repositories 接口)、presentation(common 共享组件、features/feature_name/providers、widgets、feature_screen.dart、全局 providers)、main.dart 和 app.dart 分层组织。
- 测试目录:使用 test/unit、test/widget、test/integration 分别存放单元、组件和集成测试。
- 根目录保留 pubspec.yaml、analysis_options.yaml、CLAUDE.md。
- 简单值用 Provider,简单可变状态用 StateProvider,复杂状态逻辑用 NotifierProvider,异步复杂状态用 AsyncNotifierProvider,简单异步数据用 FutureProvider,实时数据用 StreamProvider,带参数数据用 FutureProvider.family 等 family providers。
- 使用 @riverpod 注解定义 Notifier:class Users extends _$Users,@override Future<List<User>> build() async 调用 _fetchUsers()。
- 在 Notifier 的 _fetchUsers() 中通过 ref.read(userRepositoryProvider) 获取 repository 并调用 getUsers()。
- 在 Notifier 中实现 refresh():将 state 设为 const AsyncLoading(),再用 state = await AsyncValue.guard(() => _fetchUsers())。
- 在 Notifier 中实现 addUser(User user):ref.read(repository) 调用 addUser,然后 ref.invalidateSelf()。
- 在 UI 中用 ref.watch(usersProvider) 获取 AsyncValue,并用 .when(data/loading/error) 处理;error 时显示 ErrorDisplay,onRetry 调用 ref.invalidate(usersProvider)。
- 可用 switch 模式匹配 AsyncValue:AsyncData(:final value)、AsyncLoading()、AsyncError(:final error) 分别返回对应组件。
- 使用 ref.watch 让 provider 变化时重建。
- 使用 ref.read 做一次性读取,不触发重建,例如按钮回调中 ref.read(counterProvider.notifier).state++。
- 使用 ref.listen 监听变化但不重建,例如 authProvider 变为 null 时 context.go('/login')。
- 使用 ref.invalidate(usersProvider) 强制刷新 provider。
- 使用 ref.keepAlive() 防止自动 dispose,之后可通过 link.close() 允许销毁。
- 定义 Freezed 模型:导入 freezed_annotation,声明 part 'user.freezed.dart' 和 part 'user.g.dart';用 @freezed class User with _$User;const factory User({required... @Default(false) bool isActive, DateTime? createdAt}) = _User;factory User.fromJson(...) => _$UserFromJson(json)。
- 用 Freezed sealed union 定义状态:@freezed sealed class AuthState with _$AuthState,包含 initial/loading/authenticated(User)/unauthenticated/error(String) 工厂。
- 在 UI 中用 authState.when(initial/loading/authenticated/unauthenticated/error) 映射到 SplashScreen/LoadingScreen/HomeScreen/LoginScreen/ErrorScreen。
- 用 Provider<GoRouter> 创建 routerProvider,watch authProvider,避免全局 GoRouter 实例。
- 配置 GoRouter:initialLocation: '/',refreshListenable: authState,redirect 中根据是否登录和是否在 /login 跳转;未登录且不在登录页返回 '/login',已登录且在登录页返回 '/',否则返回 null。
- 配置 routes:GoRoute path '/' builder HomeScreen,嵌套 GoRoute path 'user/:id' builder UserScreen 并用 state.pathParameters['id']! 取参;单独配置 '/login';配置 errorBuilder 返回 ErrorScreen。
- 导航使用 context.go('/user/123') 跳转、context.push('/user/123') 入栈、context.pop() 出栈、context.pushReplacement('/home') 替换、context.goNamed('user', pathParameters: {'id': '123'}) 命名路由。
- 无状态组件用 ConsumerWidget:build(context, ref),用 ref.watch(userByIdProvider(userId)) 获取数据,并用 when 处理 data/loading/error。
- 有状态组件用 ConsumerStatefulWidget 和 ConsumerState:在 State 中管理 TextEditingController,在 dispose 中释放 controller,用 ref.watch(searchProvider(_controller.text)),TextField onChanged 调 setState。
- 使用 flutter_hooks 时用 HookConsumerWidget:用 useAnimationController 创建动画控制器,ref.watch(counterProvider),用 useEffect 在 count 变化时执行 controller.forward(from: 0),再用 ScaleTransition 渲染。
- 单元测试用 mocktail:创建 class MockUserRepository extends Mock implements UserRepository。
- 设置 late MockUserRepository mockRepository 和 late ProviderContainer container;setUp 中初始化 MockUserRepository,并用 ProviderContainer(overrides: [userRepositoryProvider.overrideWithValue(mockRepository)]) 覆盖依赖。
- tearDown 中调用 container.dispose()。
- 在测试中用 when(() => mockRepository.getUsers()).thenAnswer((_) async => users);通过 await container.read(usersProvider.future) 获取结果;用 expect(result, equals(users)) 断言;用 verify(() => mockRepository.getUsers()).called(1) 验证调用。
- 组件测试用 tester.pumpWidget(ProviderScope(overrides: [...], child: MaterialApp(home: ...)))。
- 覆盖 provider 返回数据:userByIdProvider('1').overrideWith((_) => AsyncData(user)),然后 expect(find.text('John'), findsOneWidget)。
- 覆盖 loading 状态:userByIdProvider('1').overrideWith((_) => const AsyncLoading()),然后 expect(find.byType(CircularProgressIndicator), findsOneWidget)。
- pubspec.yaml 顶层设置 name: my_app、description、publish_to: 'none'、version: 1.0.0+1。
- pubspec.yaml 中 environment sdk 使用 '>=3.2.0 <4.0.0'。
- dependencies 包含 flutter、flutter_riverpod ^2.4.9、riverpod_annotation ^2.3.3、freezed_annotation ^2.4.1、json_annotation ^4.8.1、go_router ^13.0.0、dio ^5.4.0、shared_preferences ^2.2.2、intl ^0.19.0。
- dev_dependencies 包含 flutter_test、build_runner ^2.4.8、freezed ^2.4.6、json_serializable ^6.7.1、riverpod_generator ^2.3.9、mocktail ^1.0.2、flutter_lints ^3.0.1。
- CI 在 push main 和 PR main 时触发。
- job build 运行于 ubuntu-latest。
- CI 步骤:actions/checkout@v4。
- 使用 subosito/flutter-action@v2,设置 flutter-version: '3.16.0'、channel: 'stable'、cache: true。
- 运行 flutter pub get 安装依赖。
- 运行 dart run build_runner build --delete-conflicting-outputs 生成代码。
- 运行 flutter analyze --fatal-infos 分析。
- 运行 flutter test --coverage 跑测试。
- 运行 flutter build apk --release 构建 APK。
- analysis_options.yaml 包含 package:flutter_lints/flutter.yaml。
- analyzer 排除 **/*.g.dart 和 **/*.freezed.dart。
- analyzer 设置 errors: invalid_annotation_target: ignore。
- analyzer language 开启 strict-casts、strict-inference、strict-raw-types。
- linter 启用 always_declare_return_types、avoid_dynamic_calls、avoid_print、avoid_type_to_string、cancel_subscriptions、close_sinks、prefer_const_constructors、prefer_const_declarations、prefer_final_locals、require_trailing_commas、unawaited_futures、use_super_parameters。
- 避免 Provider 不加 autoDispose:应使用 .autoDispose 防止内存泄漏。
- 避免在回调中使用 watch:onPressed/callbacks 中应使用 ref.read()。
- 避免在 widget 中写业务逻辑:移到 Notifiers/providers。
- 避免在 providers 中使用可变状态:用 Freezed 定义不可变模型。
- 避免不处理 AsyncValue:用 when() 处理 loading/error 状态。
- 避免在 Riverpod 中混用 setState:共享状态用 providers。
- 避免把 ref 传给函数:ref 使用保持在 widgets/providers 内。
- 避免深层嵌套 Consumer:使用 ConsumerWidget。
- 避免参数化 provider 不用 family:使用 .family 处理参数。
- 避免使用全局 GoRouter 实例:用 Provider 提供 router 并带 redirect 逻辑。
- 避免跨 async 使用 BuildContext:在 await 前保存值,不要保存 context。
- 避免忽略 dispose:在 ConsumerStatefulWidget 中清理 controllers。
适用边界 · 注意事项
- 非 Flutter/Dart 项目不适用。
- 不使用 Riverpod、Freezed、go_router、mocktail,或已有不同架构/状态管理方案的项目,不宜照搬。
- 简单一次性脚本或极小型应用可能不需要完整分层、代码生成和复杂路由。