ECC Dart/Flutter 架构模式实战指南:Repository 到 GoRouter 的分层落地规范
ECC Dart/Flutter 架构模式实战指南Repository 到 GoRouter 的分层落地规范【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECCoutput_articleECC Dart/Flutter 架构模式实战指南Repository 到 GoRouter 的分层落地规范本文基于 ECC 仓库中 Dart/Flutter Patterns 规则文档及其日文译本 docs/ja-JP/rules/dart/patterns.md展开系统梳理 Dart/Flutter 项目在 ECC 规则体系下的推荐架构模式数据层 Repository 抽象、BLoC/Cubit 与 Riverpod 双轨状态管理、依赖注入、UseCase 封装、freezed 不可变状态、清洁架构分层边界以及 GoRouter 声明式导航。读完本文你将掌握一套可直接复制到pubspec.yaml项目中的分层代码骨架并能结合 ECC 的代码评审技能skills/flutter-dart-code-review/SKILL.md对既有代码进行架构级检查。规则文档的定位它是如何进入你的项目的在 ECC 的规则体系中patterns.md属于语言特定规则层。根据 rules/README.md 的说明规则被组织为common通用层 语言特定目录两层结构rules/common/patterns.md提供与语言无关的通用模式原则如 Repository 模式、API 响应封装格式rules/dart/patterns.md则在通用原则之上补充 Dart、Flutter 及其生态特有的实现细节与代码示例。该文档头部通过 YAML frontmatter 声明其适用文件范围paths: - **/*.dart - **/pubspec.yaml这意味着任何.dart源文件或pubspec.yaml配置都在此规则的约束范围内。安装规则时需要把rules/common与rules/dart整体复制不可用/*拍平否则语言特定文件会覆盖通用文件并破坏文档内../common/相对引用当语言特定规则与通用规则冲突时语言特定规则优先specific overrides general这与 CSS 优先级或.gitignore覆盖规则类似。需要强调的是本文讨论的每一条模式都同时被 skills/flutter-dart-code-review/SKILL.md 的评审清单覆盖形成规则定标准、技能教实操的闭环。Repository 模式把数据访问封装在统一接口之后规则文档给出的核心思路与 rules/common/patterns.md 一脉相承业务逻辑依赖抽象接口而非存储机制从而支持数据源自由切换与 Mock 测试。Dart 侧的落地方案使用abstract interface class定义仓库契约abstract interface class UserRepository { FutureUser? getById(String id); FutureListUser getAll(); StreamListUser watchAll(); Futurevoid save(User user); Futurevoid delete(String id); } class UserRepositoryImpl implements UserRepository { const UserRepositoryImpl(this._remote, this._local); final UserRemoteDataSource _remote; final UserLocalDataSource _local; override FutureUser? getById(String id) async { final local await _local.getById(id); if (local ! null) return local; final remote await _remote.getById(id); if (remote ! null) await _local.save(remote); return remote; } override FutureListUser getAll() async { final remote await _remote.getAll(); for (final user in remote) { await _local.save(user); } return remote; } override StreamListUser watchAll() _local.watchAll(); override Futurevoid save(User user) _local.save(user); override Futurevoid delete(String id) async { await _remote.delete(id); await _local.delete(id); } }这份示例透露出几个值得细读的设计信号接口同时暴露 Future 与 StreamgetById/getAll是一次性读取watchAll则面向本地数据源的持续监听如数据库变更通知支撑 UI 层的响应式更新读写分离 缓存回填读取走本地优先、远程兜底策略cache-aside远程命中后回写本地写入则双写先远端后本地保证两端一致const构造器UserRepositoryImpl的构造器声明为const与 rules/dart/coding-style.md 中所有字段均为final时使用const构造器的约定一致也便于在依赖图中以常量形式注册。从测试角度看这个接口形态与 rules/dart/testing.md 中手写FakeUserRepository的方案天然契合——fake 只需要实现这 5 个方法即可无需依赖任何网络栈或数据库。状态管理之一BLoC/Cubit事件驱动与简单状态迁移规则文档给出了两种粒度的 BLoC 系写法Cubit——适合简单状态迁移直接以方法调用触发emitclass CounterCubit extends Cubitint { CounterCubit() : super(0); void increment() emit(state 1); void decrement() emit(state - 1); }BLoC——适合事件驱动的复杂流程通过sealed class建模事件全集再用copyWith维持不可变状态immutable sealed class CartEvent {} class CartItemAdded extends CartEvent { CartItemAdded(this.item); final Item item; } class CartItemRemoved extends CartEvent { CartItemRemoved(this.id); final String id; } class CartCleared extends CartEvent {} immutable class CartState { const CartState({this.items const []}); final ListItem items; CartState copyWith({ListItem? items}) CartState(items: items ?? this.items); } class CartBloc extends BlocCartEvent, CartState { CartBloc() : super(const CartState()) { onCartItemAdded((event, emit) emit(state.copyWith(items: [...state.items, event.item]))); onCartItemRemoved((event, emit) emit(state.copyWith(items: state.items.where((i) i.id ! event.id).toList()))); onCartCleared((_, emit) emit(const CartState())); } }这段代码与 rules/dart/coding-style.md 的多条规范互相印证Dart 3sealed类CartEvent被声明为sealed意味着编译器可以穷尽检查所有事件分支杜绝未知事件的遗漏处理不可变性状态类字段final集合变更一律通过copyWith产生新实例绝不原地修改itemsconst集合字面量this.items const []作为默认值。配套的单元测试方案在 rules/dart/testing.md 中有完整示例使用bloc_test的blocTestCartBloc, CartState分别验证CartItemAdded后发出的状态序列与CartCleared的种子状态重置expect中逐项断言emit序列。评审层面flutter-dart-code-review 技能 还提醒BLoC 之间不应直接相互依赖跨组件通信应共享 Repository 或在展示层协调所有状态迁移loading → success、loading → error、retry都必须有对应测试。状态管理之二RiverpodProvider 图与 Notifier规则文档展示了 Riverpod 三种核心构件异步 Provider——用ref.watch表达 provider 间依赖riverpod FutureListUser users(Ref ref) async { final repo ref.watch(userRepositoryProvider); return repo.getAll(); }可变状态的 Notifier——build()返回初始值通过state 赋值触发重建riverpod class CartNotifier extends _$CartNotifier { override ListItem build() []; void add(Item item) state [...state, item]; void remove(String id) state state.where((i) i.id ! id).toList(); void clear() state []; }ConsumerWidget——UI 消费端只做声明式读取不持有任何业务逻辑class CartPage extends ConsumerWidget { const CartPage({super.key}); override Widget build(BuildContext context, WidgetRef ref) { final items ref.watch(cartNotifierProvider); return ListView( children: items.map((item) CartItemTile(item: item)).toList(), ); } }在测试侧rules/dart/testing.md 建议用ProviderContaineroverrides注入 fakefinal container ProviderContainer( overrides: [userRepositoryProvider.overrideWithValue(FakeUserRepository())], ); addTearDown(container.dispose); final result await container.read(usersProvider.future); expect(result, isNotEmpty);Widget 测试则通过ProviderScope(overrides: [...])覆盖 provider再以tester.pumpWidget验证 UI 行为。评审技能还给出了一个值得记住的原则对照表Riverpod 中 provider 依赖 provider 是预期行为但要警惕循环依赖或过度缠绕的链条选择器ref.watch(p.select(...))应尽量窄化重建范围。依赖注入构造器注入优先组合根统一注册规则文档明确偏好构造器注入并在组合根composition root使用get_it或 Riverpod provider 完成装配void setupDependencies() { final di GetIt.instance; di.registerSingletonApiClient(ApiClient(baseUrl: Env.apiUrl)); di.registerSingletonUserRepository( UserRepositoryImpl(diApiClient(), diLocalDatabase()), ); di.registerFactory(() UserListViewModel(diUserRepository())); }注意这里区分了两种生命周期registerSingleton全局唯一实例适合无状态的ApiClient和贯穿全 App 的UserRepositoryregisterFactory每次解析都新建适合承载页面级状态的ViewModel。flutter-dart-code-review 技能 对 DI 的评审要点包括分层边界处类应依赖抽象接口而非具体实现依赖必须由外部注入构造器、DI 框架或 provider 图禁止在类内部自行创建注册要区分 singleton/factory/lazy singleton避免 DI 图循环依赖服务定位器调用不应散落在业务逻辑各处。与之呼应的是本仓库的代码生成规则rules/dart/coding-style.md要求.g.dart、.freezed.dart等生成文件要么提交、要么统一 gitignore且绝不手工编辑。ViewModel 模式不依赖 BLoC/Riverpod 的轻量方案规则文档提供了一种仅基于 Flutter 内置ChangeNotifier的轻量状态容器适合不想引入状态管理框架的场景。其核心是用密封状态类表达加载中 / 成功 / 失败三种互斥状态class UserListViewModel extends ChangeNotifier { UserListViewModel(this._repository); final UserRepository _repository; AsyncStateListUser _state const Loading(); AsyncStateListUser get state _state; Futurevoid load() async { _state const Loading(); notifyListeners(); try { final users await _repository.getAll(); _state Success(users); } on Exception catch (e) { _state Failure(e); } notifyListeners(); } }这里的AsyncState正是 rules/dart/coding-style.md 中定义的密封类层次Loading/Success/Failure。coding-style 文档特别强调UI 层必须对密封类型做穷尽式switch禁止用if (state is Loading)的零散判断也不允许出现默认分支——这样不可能的状态如同时 isLoading 且 hasError在编译期就无法表达。on Exception catch (e)的写法也与错误处理规范一致永远指定异常类型绝不写裸catch (e)也绝不捕获Error子类型它们表示编程缺陷。UseCase 模式把业务规则搬出 UI 与状态管理器规则文档给出的 UseCase 是单方法类通过实现call让实例可被直接调用class GetUserUseCase { const GetUserUseCase(this._repository); final UserRepository _repository; FutureUser? call(String id) _repository.getById(id); } class CreateUserUseCase { const CreateUserUseCase(this._repository, this._idGenerator); final UserRepository _repository; final IdGenerator _idGenerator; // 注入 —— 领域层不应直接依赖 uuid 包 Futurevoid call(CreateUserInput input) async { // 校验、应用业务规则然后持久化 final user User(id: _idGenerator.generate(), name: input.name, email: input.email); await _repository.save(user); } }两个细节值得注意依赖注入贯穿用例层CreateUserUseCase的 ID 生成器由外部注入领域层因此不直接依赖uuid包保持纯 Dart 可测试性展示层只调用用例、不直连仓库这是清洁架构分层纪律的体现见下文也是评审技能中业务逻辑必须位于 widget 层之外的具体落实。freezed 不可变状态代码生成替代手写样板规则文档推荐用freezed生成不可变状态类省去手写、hashCode、copyWith的样板代码freezed class UserState with _$UserState { const factory UserState({ Default([]) ListUser users, Default(false) bool isLoading, String? errorMessage, }) _UserState; }Default([])与Default(false)提供了字段默认值生成的类自带值语义value equality与copyWith。评审技能在不可变性与值相等一节提醒无论用Equatable、freezed、Dart records 还是手写/hashCode机制必须在项目内保持一致状态对象内部集合不得以裸可变List/Map暴露。生成文件.freezed.dart应遵循 coding-style 的代码生成策略统一处理且freezed注解只保留在规范源文件上。清洁架构分层边界domain / data / presentation 三层的纪律规则文档给出了推荐的目录骨架lib/ ├── domain/ # 纯 Dart —— 不依赖 Flutter不依赖外部包 │ ├── entities/ │ ├── repositories/ # 抽象接口 │ └── usecases/ ├── data/ # 实现领域接口 │ ├── datasources/ │ ├── models/ # 带 fromJson/toJson 的 DTO │ └── repositories/ └── presentation/ # Flutter 组件 状态管理 ├── pages/ ├── widgets/ └── providers/ (or blocs/ or viewmodels/)伴随三条硬性纪律领域层不得import package:flutter或任何数据层包——保持纯 Dart保证可单测、可移植数据层在仓库边界处把 DTO 映射为领域实体——DTO 的fromJson/toJson只存在于data/models不向上泄漏展示层调用用例UseCase不直接使用仓库——UI 只关心做什么不关心怎么做。这套结构与 rules/dart/testing.md 的测试目录组织一一对应test/unit/domain/use_cases/、test/unit/data/repositories/、test/widget/presentation/pages/并且与评审技能中特性优先还是分层优先的目录结构必须保持一致的检查项呼应。import 规范上rules/dart/coding-style.md 要求跨特性、跨层一律使用package:导入禁用../相对导入顺序为dart:→ 外部package:→ 内部package:。导航GoRouter 声明式路由与认证重定向规则文档推荐用GoRouter做声明式导航并给出了带认证守卫的完整配置final router GoRouter( routes: [ GoRoute( path: /, builder: (context, state) const HomePage(), ), GoRoute( path: /users/:id, builder: (context, state) { final id state.pathParameters[id]!; return UserDetailPage(userId: id); }, ), ], // refreshListenable 会在认证状态变化时重新评估 redirect refreshListenable: GoRouterRefreshStream(authCubit.stream), redirect: (context, state) { final isLoggedIn context.readAuthCubit().state is AuthAuthenticated; if (!isLoggedIn !state.matchedLocation.startsWith(/login)) { return /login; } return null; }, );三个关键点路径参数/users/:id通过state.pathParameters[id]!取参——这里使用!是因为路由匹配已保证参数存在属于空值即编程错误的合理场景coding-style 允许在确属编程错误时使用 bang operatorrefreshListenableredirect组合把认证状态流authCubit.stream接入GoRouterRefreshStream登录状态一变即重新执行 redirect实现集中式路由守卫避免在各页面重复判断state.matchedLocation白名单对/login自身放行防止重定向死循环。评审技能对导航的补充要求全项目只使用一种路由方案禁止Navigator.push与声明式路由混用路由参数必须是强类型而非MapString, dynamic路径定义成常量或枚举避免魔法字符串深链 URL 在导航前必须校验与清洗。相关资源从模式到落地的一站式入口规则文档末尾rules/dart/patterns.md#L258-L261指向了两项配套技能skills/flutter-dart-code-review/SKILL.md库无关的 Flutter/Dart 代码评审清单覆盖组件最佳实践、状态管理BLoC、Riverpod、Provider、GetX、MobX、Signals、性能、无障碍、安全与清洁架构并附有一张通用原则 × 各状态管理方案的对照速查表skills/compose-multiplatform-patternsKotlin Multiplatform 与 Flutter 的互操作模式参考。若要把本文的模式投入日常开发还可配套查阅同目录下的姊妹规则rules/dart/coding-style.md格式化、命名、空安全、Dart 3 模式匹配、rules/dart/testing.mdflutter_test/bloc_test/fake_async/golden 测试与覆盖率门槛、rules/dart/hooks.mddart format/dart analyze/flutter test的自动化钩子配置。整套规则遵循语言特定优先于通用的覆盖原则并可通过./install.sh dart安装到项目详见 rules/README.md。本文所有代码均可在 Dart/Flutter 项目中直接落地先按清洁架构建目录再以 Repository UseCase 搭数据与业务层按团队偏好选 BLoC/Riverpod/ChangeNotifier 之一管理状态最后用 GoRouter 统一导航——这正是 ECC 规则体系为 Dart/Flutter 团队预设的可执行架构基线。 /output_article【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考