Appearance
Flutter 项目实战路线
推荐用一个待办事项 App 作为练习项目。它足够小,可以独立完成;又足够完整,能覆盖真实 Flutter 工程里的关键问题。
这个练习项目的目标不是做一个漂亮 demo,而是把 Flutter 项目从“页面能跑”推进到“工程可维护”。完成后应该能回答:
- 页面和组件怎么拆。
- 状态放在哪里,什么时候释放。
- 接口、缓存、错误和重试怎么统一。
- 多环境、测试、发布和线上观测怎么形成闭环。
- 出现白屏、卡顿、构建失败时从哪里查。
功能范围
- 登录与用户信息。
- 待办列表、详情、创建、编辑、删除。
- 搜索、筛选、分页。
- 本地缓存和离线草稿。
- 错误重试和空状态。
- 基础主题和组件库。
- 单元测试、Widget 测试和构建发布。
建议按迭代推进:
| 迭代 | 内容 | 目标 |
|---|---|---|
| 第 0 轮 | 需求边界、平台目标、接口契约、验收标准 | 明确项目能不能按当前条件交付 |
| 第 1 轮 | 本地假数据、列表、详情、空状态 | 跑通页面结构和组件拆分 |
| 第 2 轮 | 接口层、错误态、下拉刷新、分页 | 跑通网络和状态流转 |
| 第 3 轮 | 本地缓存、离线草稿、登录态 | 跑通数据边界和持久化 |
| 第 4 轮 | 主题、组件库、动画、可访问性 | 提升体验和一致性 |
| 第 5 轮 | 测试、CI、多环境、打包发布 | 形成工程闭环 |
第 0 轮很重要。很多项目失败不是因为 Flutter 页面写不出来,而是平台能力、接口契约、发布渠道、权限文案、多环境配置没有提前确认。第 0 轮应该产出一份简短说明:目标平台、核心流程、接口清单、环境清单、权限清单、发布方式和验收标准。
推荐目录
text
lib/
core/
network/
storage/
logging/
error/
shared/
widgets/
theme/
features/
auth/
task/
presentation/
application/
data/
domain/presentation 放页面和组件,application 放状态和用例编排,data 放接口、缓存和 Repository 实现,domain 放业务模型和规则。
可以把每层职责再说清楚:
| 层 | 典型内容 | 不应该做什么 |
|---|---|---|
presentation | Page、Widget、Controller、ViewState | 不直接拼接口 URL,不直接操作数据库 |
application | 用例编排、状态管理、页面命令 | 不写具体 HTTP、SQLite 实现 |
domain | 业务实体、值对象、规则、接口定义 | 不依赖 Flutter UI 和平台插件 |
data | API、DTO、缓存、Repository 实现 | 不直接控制页面跳转和 UI |
core | 网络、日志、错误、配置、存储基础设施 | 不放具体业务页面 |
shared | 通用组件、主题、工具 | 不沉淀业务专属逻辑 |
小项目不必机械分太多层,但边界要清楚。最重要的是页面不直接知道数据来自网络、缓存还是本地假数据;业务规则不依赖某个具体 Widget;底层插件不散落在各个页面里。
需求到任务的拆解
一个功能不要直接拆成“写页面”。更可靠的拆法是从业务路径拆到工程任务。
| 业务点 | 工程任务 |
|---|---|
| 用户登录 | 表单校验、登录接口、token 存储、刷新 token、退出登录 |
| 待办列表 | 列表页面、分页、下拉刷新、空状态、错误态、缓存读取 |
| 待办详情 | 路由参数、详情接口、无权限、数据不存在、操作按钮状态 |
| 新建待办 | 表单状态、草稿、重复提交、接口错误、创建成功后列表刷新 |
| 离线编辑 | 本地队列、同步状态、冲突处理、失败重试 |
每个功能都应该补齐四件事:页面表现、数据来源、异常分支、验证方式。只写成功路径,会让项目在联调、测试和上线阶段集中暴露问题。
状态设计
每个页面至少定义清楚四类状态:
| 状态 | 页面表现 |
|---|---|
| Loading | 骨架屏或加载指示 |
| Success | 正常内容 |
| Empty | 空数据说明和下一步动作 |
| Error | 错误原因、重试入口、必要日志 |
复杂页面可以进一步拆出 refreshing、loadingMore、offline、partialError 等状态。
一个页面状态可以这样表达:
dart
sealed class TaskListState {
const TaskListState();
}
class TaskListLoading extends TaskListState {
const TaskListLoading();
}
class TaskListSuccess extends TaskListState {
const TaskListSuccess(this.tasks, {this.refreshing = false});
final List<Task> tasks;
final bool refreshing;
}
class TaskListEmpty extends TaskListState {
const TaskListEmpty();
}
class TaskListFailure extends TaskListState {
const TaskListFailure(this.message);
final String message;
}这样写的好处是页面状态可穷举,UI 不容易漏掉分支。真实项目里可以根据状态管理库调整写法,但“状态可描述、分支可覆盖、错误可恢复”的原则不变。
状态对象还要处理动作,而不只是保存数据。例如列表页常见动作包括:首次加载、下拉刷新、加载更多、切换筛选、重试、删除、恢复、进入详情。每个动作都要明确:
- 触发后哪些状态会变化。
- 是否允许重复触发。
- 失败后页面保留旧数据还是清空。
- 页面退出时是否需要取消请求、订阅或定时器。
- 是否需要记录日志或埋点。
接口与缓存
Repository 是数据边界。页面不直接知道数据来自网络还是本地:
- 先从缓存读取可用数据。
- 同时发起网络刷新。
- 网络成功后写回缓存。
- 网络失败时展示可恢复错误。
- 离线写入进入同步队列。
推荐的数据流:
text
Page/Widget
-> State/Controller
-> UseCase
-> Repository
-> RemoteDataSource
-> LocalDataSource接口返回的数据通常不应该直接当页面模型使用。更稳的做法是:
| 对象 | 作用 |
|---|---|
| DTO | 对应接口 JSON,处理字段命名、可空和兼容 |
| Entity | 业务模型,表达项目内部稳定语义 |
| ViewModel/ViewState | 面向页面展示,包含格式化后的文本、按钮状态 |
例如接口返回 due_at,业务模型可以是 DateTime dueAt,页面模型可以进一步变成 今天 18:00 或 已逾期 2 小时。这样接口变化、业务规则和页面展示不会绑死在一起。
网络、错误和重试
工程项目里不要让每个页面各自处理 HTTP 状态码。建议建立统一错误模型:
| 错误类型 | 例子 | 页面策略 |
|---|---|---|
| 网络不可用 | 无网络、DNS、超时 | 显示重试,保留缓存 |
| 认证失败 | token 过期、账号被踢 | 跳转登录或刷新 token |
| 权限不足 | 无访问权限 | 给出明确说明 |
| 业务错误 | 参数不合法、状态不可操作 | 展示业务文案 |
| 服务异常 | 5xx、网关错误 | 可重试并记录日志 |
| 解析异常 | 字段缺失、类型不匹配 | 记录详情,降级展示 |
重试要有边界。加载失败可以让用户点重试;后台同步可以做指数退避;创建、支付、删除这类动作要考虑幂等和重复提交,不能简单循环重试。
本地存储与离线
Flutter 常见本地存储可以按需求选择:
| 需求 | 可选方案 |
|---|---|
| 少量偏好设置 | shared_preferences |
| token、安全信息 | 安全存储插件 |
| 结构化业务数据 | SQLite、Drift、Isar、Hive 等 |
| 图片和文件缓存 | 文件系统、缓存管理插件 |
| 大量离线同步 | 本地数据库 + 同步队列 |
离线不是“缓存一下列表”这么简单。至少要明确:
- 缓存什么时候读取,什么时候刷新。
- 数据过期后如何提示用户。
- 离线创建或编辑如何进入同步队列。
- 同步失败如何重试、合并或让用户处理冲突。
- 退出登录时哪些本地数据要清理。
路由与页面组织
小项目可以用 Navigator 和命名路由;复杂项目建议使用支持声明式路由、深链和守卫能力的路由方案。无论使用什么库,都要明确:
| 问题 | 说明 |
|---|---|
| 页面参数从哪里来 | 列表进入详情、外部链接进入页面 |
| 未登录如何处理 | 跳登录、返回原目标、刷新用户信息 |
| 返回栈如何维护 | tab、详情、多级页面、弹窗 |
| 深链如何解析 | App 冷启动、前台、后台唤醒 |
| 错误页面如何兜底 | 参数缺失、资源不存在、权限不足 |
路由不只是页面跳转 API,它是应用状态的一部分。尤其在移动端,系统返回键、手势返回、深链唤醒、登录过期都会影响路由设计。
组件库与主题
项目做大后,不要让每个页面各自定义颜色、字号、圆角和间距。建议建立主题和基础组件:
| 内容 | 示例 |
|---|---|
| Design Token | 颜色、字号、间距、圆角、阴影 |
| 基础组件 | Button、Input、Dialog、EmptyView、ErrorView |
| 业务组件 | TaskCard、UserAvatar、StatusBadge |
| 页面模板 | 列表页、表单页、详情页、设置页 |
组件库的价值不是堆组件,而是统一交互和错误处理。例如所有列表页都应该有一致的加载、空数据、失败、刷新和分页体验。这样新页面不需要重新发明状态 UI,质量也更稳定。
权限、资源和国际化
项目进入真实设备后,要补齐平台边界:
| 内容 | 关注点 |
|---|---|
| 权限 | 相机、相册、定位、通知、蓝牙等权限的申请、拒绝、永久拒绝 |
| 资源 | 图片、字体、启动图、应用图标、不同分辨率和暗色模式 |
| 国际化 | 文案长度、日期、数字、货币、复数、从右到左语言 |
| 可访问性 | 语义标签、字体缩放、点击区域、颜色对比 |
权限不要只处理“同意”分支。用户拒绝权限、永久拒绝权限、从系统设置回来、不同系统版本权限行为不一致,都是常见问题。资源也不要只看模拟器,真机上的启动图、图标、字体和图片内存更容易暴露问题。
测试和发布
最小质量门禁:
bash
dart format --set-exit-if-changed .
flutter analyze
flutter test发布前确认:
- 多环境配置没有串。
- Android 和 iOS 签名可追溯。
- 权限文案和隐私说明完整。
- 崩溃、日志、性能和关键埋点已接入。
- 发布记录包含版本、提交、环境和回滚方案。
测试可以分层:
| 测试类型 | 覆盖内容 |
|---|---|
| 单元测试 | 业务规则、格式化、Repository 策略、错误映射 |
| Widget 测试 | 页面状态、按钮行为、表单校验、空/错/成功态 |
| 集成测试 | 登录、列表、创建、离线、关键业务流程 |
| Golden 测试 | 视觉稳定性要求高的组件 |
CI 中至少跑:
bash
dart format --set-exit-if-changed .
flutter analyze
flutter test发布前再补:
bash
flutter build apk --release
flutter build appbundle --release
flutter build ios --release不同团队会有不同发布平台,但原则相同:版本可追溯、环境可区分、构建可复现、失败可回滚。
线上观测
Flutter 项目上线后,需要能回答这些问题:
- 启动耗时是否变慢。
- 某个版本崩溃率是否升高。
- 关键页面是否白屏或接口失败。
- 低端机是否掉帧严重。
- 某个插件是否在特定系统版本上异常。
常见观测点:
| 类型 | 内容 |
|---|---|
| 崩溃 | Dart 异常、原生崩溃、插件异常 |
| 性能 | 启动耗时、帧率、慢页面、图片加载 |
| 网络 | 接口耗时、错误率、重试次数 |
| 行为 | 页面访问、按钮点击、关键流程转化 |
| 日志 | 用户、设备、版本、环境、请求 ID |
日志和埋点要注意隐私边界,不要随意上报 token、手机号、身份证、完整地址等敏感信息。
实战完成标准
完成待办事项 App 后,可以用下面清单自检:
- 有需求拆解说明,能解释功能范围和不做什么。
- 有多环境配置方案,能区分开发、测试和生产。
- 能解释项目目录每一层职责。
- 页面包含加载、空数据、成功、失败和重试。
- 表单、权限、弱网、登录过期、分页末页都有处理。
- 接口层、缓存层和页面层没有直接耦合。
- 有统一错误模型,不是每个页面各写一套异常处理。
- 有基本主题和复用组件。
- 有至少一组单元测试和 Widget 测试。
flutter analyze和flutter test能通过。- Android/iOS 发布配置有文档记录。
- 线上崩溃、日志、性能和关键埋点有接入方案。
- 有交接文档,包含启动命令、环境变量、发布步骤、常见问题和回滚方式。
