基于 Flutter 官方推荐的架构模式与最佳实践
- 项目概述
- 核心架构原则
- 分层架构设计
- MVVM 架构模式
- UI 层实现规范
- 数据层实现规范
- 依赖注入规范
- 项目结构组织
- 核心库使用规范
- 编码最佳实践
- 错误与异常处理规范
- 测试策略
- 开发工作流
@PRD.md
架构技术栈:
- 架构模式:MVVM + Repository Pattern
- 状态管理:ChangeNotifier + ListenableBuilder
- 命令模式:flutter_command
- 结果处理:result_dart
- 依赖注入:provider
- 路由管理:go_router
- 数据模型:freezed
- 主题:Material Design 3,仅通过主题定义样式(禁止硬编码颜色、字号等)
工程约束:
- 变更代码后,必须执行 flutter analyze。禁止执行 flutter run。
- 所有样式必须来源于主题,不得硬编码颜色、字号、边距等。
- 关注点分离
- UI 逻辑与业务逻辑分离
- 数据获取与展示分离
- 按功能模块组织代码
- 单一职责类
- 单一数据源(SSOT)
- 每种数据类型由单一 Repository 负责
- 统一数据管理,避免重复与不一致
- 单向数据流
- [数据层] → [逻辑层] → [UI 层],用户操作通过 Command 反向触发数据更新
- 声明式 UI
- 界面完全由状态驱动:[UI = f(State)]
- 分层依赖原则
- ViewModel → Repository → Service
- 禁止 ViewModel 直接依赖 Service
- 全局共享数据管理
- 通过共享的 Repository 实例统一管理
- ViewModel 解耦
- ViewModel 之间禁止直接依赖,通信通过 Repository/Stream
- Repository 通知机制
- Repository 使用 StreamController 暴露数据变更,禁止继承 ChangeNotifier
三层架构(与参考项目一致):
- UI 层(Views & ViewModels):展示与交互;通过 flutter_command 执行操作
- 逻辑层(Use Cases,可选):封装复杂业务逻辑、协调多仓库
- 数据层(Repositories & Services):数据管理、来源整合、结果处理、缓存策略
职责矩阵:
| 层次 | 组件 | 职责 |
|---|---|---|
| UI 层 | View, ViewModel | 页面展示、用户交互、状态管理 |
| 逻辑层 | Use Cases | 封装复杂业务、跨 Repository 协调 |
| 数据层 | Repository, Service | 统一数据访问、来源抽象与实现、错误与缓存 |
关系规则:
- 每个功能页面对应一个 View 与一个 ViewModel
- ViewModel 持有 Repository 的引用(或多个)
- Repository 可被多个 ViewModel 共享
- Service 专注单一数据源(如邮件、CSV、本地存储、URL Scheme 集成通道)
View 规范:
- 构造函数仅接收 key 与 viewModel(或以 Provider 注入)
- 处理 Command 的执行态、完成态与错误态
- 不包含业务逻辑
- 所有用户交互经由 Command 执行
ViewModel 规范:
- 继承 ChangeNotifier
- 私有 final Repository 成员
- 使用 flutter_command 暴露用户动作(异步/同步)
- UI 状态 private setter + public getter
- 在构造中初始化 Commands
- 使用全局 appLogger 记录关键操作、错误与状态变更
Command 监听器规范:
- 在 initState 中订阅 results 与 errors 流
- 在 dispose 中取消订阅
- 使用 mounted 检查 Widget 生命周期
主题与样式:
- 遵循 Material Design 3
- 仅通过主题定义颜色、字号、密度、形状与间距;禁止硬编码
- 提供暗色 / 亮色方案和动态色适配(可选)
无障碍与国际化:
- 遵循 a11y 最佳实践(语义标签、可达性焦点顺序)
- 预留多语言能力(arb 文件与本地化代理)
Repository 规范:
- 统一返回 Result
- 负责缓存、错误与重试、模型转换(API/来源数据 → 领域模型)
- 多来源整合(邮件/CSV/本地等)在仓库层组合
- 通过 StreamController 广播数据变更
- 禁止继承 ChangeNotifier
Service 规范:
- 无状态类
- 单一数据源职责(如 MailIngestService、CsvIngestService、LocalStoreService、UrlSchemeService)
- 返回 Result
- 统一错误处理与边界封装
- 私有底层依赖(如 Dio、SharedPreferences、平台通道)
来源扩展约束(可拓展性):
- 为新来源定义独立 Service,遵循单一职责
- Repository 以组合/策略模式接入新增来源
- 对外 API 与领域模型稳定,新增来源不影响上层调用
- 使用接口/抽象类对来源进行契约约束,便于插件化扩展
- 使用 Provider 完成依赖注入(应用级、模块级、页面级)
- 构造函数注入 Repository 至 ViewModel
- 在 config/dependencies.dart 统一注册与装配
- 提供测试替身注入入口(Mocks/Fakes)
建议目录:
lib/
├── main.dart # 应用入口
├── main_viewmodel.dart # 应用级 ViewModel(主题/会话等)
│
├── config/
│ ├── dependencies.dart # 依赖注入装配
│ └── env.dart # 运行环境配置(可选)
│
├── ui/
│ ├── core/
│ │ ├── theme.dart # M3 主题配置(禁止硬编码)
│ │ ├── main_layout.dart # 通用布局
│ │ └── ui/ # 通用组件(加载、错误、空态等)
│ │
│ ├── bills_list/ # 账单列表视图
│ │ ├── view_models/
│ │ └── widgets/
│ │
│ ├── bills_calendar/ # 账单日历视图
│ │ ├── view_models/
│ │ └── widgets/
│ │
│ └── settings/ # 设置与导入配置
│ ├── view_models/
│ └── widgets/
│
├── domain/
│ ├── models/ # 领域模型(freezed)
│ │ ├── bill/
│ │ └── source/ # 来源/渠道定义与枚举
│ └── use_cases/ # 复杂业务用例(可选)
│
├── data/
│ ├── repository/
│ │ ├── bill/ # BillRepository(SSOT)
│ │ └── settings/ # 配置相关仓库
│ └── service/
│ ├── mail_ingest_service.dart # 邮件导入
│ ├── csv_ingest_service.dart # CSV 导入
│ ├── local_store_service.dart # 本地存储(数据库/Prefs)
│ ├── url_scheme_service.dart # URL Scheme 集成(导入到一木记账)
│ └── platform/ # 平台通道封装(如 iOS/Android)
│
├── routing/
│ ├── router.dart # go_router 配置
│ └── routes.dart # 路由常量
│
└── utils/
├── logger.dart # appLogger
├── exceptions.dart # 统一异常
└── option_data.dart # 工具
测试与资源:
test/
├── unit/
│ ├── data/
│ ├── domain/
│ └── ui/
├── widget/
├── integration/
└── helpers/
test_resources/
├── mocks/
└── fixtures/
命名规范(与参考保持一致):
| 类型 | 命名 | 示例 |
|---|---|---|
| ViewModel | {Feature}ViewModel | BillsListViewModel |
| Screen | {Feature}Screen | BillsCalendarScreen |
| Repository | {Domain}Repository | BillRepository |
| Service | {Purpose}Service | MailIngestService |
| Model | {Entity}/{Entity}ApiModel | Bill/BillCsvModel |
| UseCase | {Action}UseCase | MergeBillSourcesUseCase |
| Exception | {Context}Exception | IngestFailedException |
- flutter_command:所有交互动作以 Command 暴露,使用 results/errors 订阅
- result_dart:所有失败可能的操作统一返回 Result;在 VM 中使用 isSuccess/getOrNull/exceptionOrNull 处理
- provider:多层级依赖注入,支持测试替身
- freezed:所有模型不可变,集中定义 copyWith/equality 与序列化
- go_router:集中路由与导航栈管理
强制要求:
- 分层清晰:View 不直接访问 Repository,所有数据经 ViewModel
- MVVM:每个页面 1:1 的 View 与 ViewModel
- Command 模式:用户交互通过 Command.execute()
- Result 模式:错误路径明确、可测试
- 依赖注入:Provider 装配,构造注入依赖
- 不可变模型:使用 freezed
- 主题化:无硬编码样式,统一从 Theme 中取值
- 每次代码修改后执行 flutter analyze;切勿执行 flutter run
推荐实践:
- Use Case:封装跨仓库的复杂流程
- 缓存策略:仓库实现内存/本地缓存
- 离线支持:本地存储容错与回放
- 统一日志:appLogger 在 VM/Repository 关键路径打点
禁止数据重复缓存:
- ViewModel 不得缓存来自 Repository 的业务数据
- UI 组件不得维护自己的状态数据
- 所有数据变更必须通过 Repository 进行
✅ 正确做法:
class LibraryViewModel extends ChangeNotifier {
final GameRepository _gameRepository;
// 仅保留UI专用状态
bool _isLoading = false;
String _searchQuery = '';
// 动态从Repository获取数据
List<Game> get games => _getFilteredGames();
Map<int, GameStatus> get gameStatuses => _gameRepository.gameStatuses;
List<Game> _getFilteredGames() {
// 实时从Repository获取并筛选
return _gameRepository.gameLibrary.where(...);
}
}❌ 错误做法:
class LibraryViewModel extends ChangeNotifier {
// 违规:缓存了Repository中的数据
List<Game> _games = [];
Map<int, GameStatus> _gameStatuses = {};
// 违规:返回缓存的数据而非实时获取
List<Game> get games => _games;
}✅ 正确做法:
class GameListWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<LibraryViewModel>(
builder: (context, viewModel, child) {
// 直接从ViewModel获取数据
final games = viewModel.games;
return ListView.builder(...);
},
);
}
}❌ 错误做法:
class GameListWidget extends StatefulWidget {
State<GameListWidget> createState() => _GameListWidgetState();
}
class _GameListWidgetState extends State<GameListWidget> {
// 违规:在Widget中缓存数据
List<Game> _localGames = [];
void initState() {
// 违规:从ViewModel复制数据到本地
_localGames = context.read<LibraryViewModel>().games;
}
}Repository 负责:
- 数据的唯一存储和缓存
- 数据变更的统一管理
- 通过 Stream 广播数据变更
- 数据持久化和同步
ViewModel 负责:
- UI 状态管理(加载、错误、筛选条件等)
- 将用户操作转换为 Repository 调用
- 数据的展示逻辑(筛选、排序、格式化)
UI 组件 负责:
- 纯展示和用户交互
- 不维护任何业务状态
- 通过 ViewModel 获取所有数据
ViewModel 检查项:
- 是否缓存了 Repository 中存在的数据?
- getter 是否直接返回缓存字段而非从 Repository 获取?
- 是否监听 Repository 的 Stream 并更新本地缓存?
- 状态更新方法是否同时更新本地缓存和 Repository?
UI 组件检查项:
- StatefulWidget 是否存储了业务数据?
- 是否从 ViewModel 复制数据到本地变量?
- 是否直接调用 Repository 而跳过 ViewModel?
- 是否使用 Consumer/Provider 正确获取最新数据?
Repository 检查项:
- 是否通过 Stream 暴露数据变更?
- 数据是否只在 Repository 中存在单一副本?
- 是否正确处理数据的增删改查?
模式 1:ViewModel 缓存 Repository 数据
// 违规
class MyViewModel {
List<Game> _games = [];
void loadGames() {
_games = _repository.gameLibrary; // 创建了数据副本
}
}
// 修复
class MyViewModel {
List<Game> get games => _repository.gameLibrary; // 直接获取
}模式 2:UI 组件状态缓存
// 违规
class _MyWidgetState {
List<Game> _localGames = [];
void initState() {
_localGames = widget.viewModel.games;
}
}
// 修复
class MyWidget {
Widget build(context) {
return Consumer<MyViewModel>(
builder: (context, viewModel, child) {
return ListView(children: viewModel.games.map(...));
},
);
}
}模式 3:重复的数据同步逻辑
// 违规
class MyViewModel {
void updateGame(Game game) {
_repository.updateGame(game);
_localGames = _repository.gameLibrary; // 重复同步
}
}
// 修复
class MyViewModel {
void updateGame(Game game) {
_repository.updateGame(game); // Repository通过Stream通知变更
// ViewModel通过getter自动获取最新数据
}
}统一错误页面组件:
- 位置:lib/ui/core/ui/error_page.dart
- 所有错误态统一走 ErrorPage 工厂方法(如 ErrorPage.fromException(e))
- 异常分层:Service 抛领域内可解释异常,Repository 转换与聚合后返回 Result 失败分支
- 主题一致:错误组件遵循主题与密度体系
- 用户反馈:清晰提示与可选重试入口
异常分型建议:
- IngestFailedException(来源导入失败)
- ParseException(CSV/邮件解析失败)
- NetworkException(网络错误)
- StorageException(本地存储错误)
- UrlSchemeException(外部导入/跳转错误)
- UnsupportedSourceException(不受支持的来源/格式)
覆盖要求:
- Repository 层与 ViewModel 层 100% 覆盖率目标
- 关键 Widget 的 UI 测试
- Command 执行流与错误路径测试
- Result 处理逻辑测试
- 路由与深链(URL Scheme)集成测试(可在集成测试中验证)
测试资源:
- 使用 fixtures 提供 CSV/邮件样本
- 使用 mocks/stubs 替代外部依赖(Service/平台通道)
- 版本与提交:采用 semantic-release,提交信息遵循 Conventional Commits
- feat/fix/refactor/docs/style/test/perf/ci/build/chore/revert
- TDD:先写失败测试,再实现功能
- 代码质量:每次提交前执行 flutter analyze
- 代码生成:涉及 Mockito/Freezed 等代码生成问题时,先运行 flutter pub run build_runner build
附加约束与说明
- URL Scheme 集成仅定义在 Service 与 Repository 层对外的抽象契约;UI 通过 ViewModel 的 Command 触发
- 多来源导入需通过统一的领域模型输出,来源差异在 Service 内部处理
- 如需新增来源/视图,严格遵循现有分层与依赖规则,确保对上层无破坏性变更
本规范为项目基础骨架与约束,后续可在不改变架构与技术栈前提下增补模块级细则与代码模板。
参考 api 文档 https://igdb-openapi.s-crypt.co/#/
接口频控: 4 requests per second