Skip to content

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 放业务模型和规则。

可以把每层职责再说清楚:

典型内容不应该做什么
presentationPage、Widget、Controller、ViewState不直接拼接口 URL,不直接操作数据库
application用例编排、状态管理、页面命令不写具体 HTTP、SQLite 实现
domain业务实体、值对象、规则、接口定义不依赖 Flutter UI 和平台插件
dataAPI、DTO、缓存、Repository 实现不直接控制页面跳转和 UI
core网络、日志、错误、配置、存储基础设施不放具体业务页面
shared通用组件、主题、工具不沉淀业务专属逻辑

小项目不必机械分太多层,但边界要清楚。最重要的是页面不直接知道数据来自网络、缓存还是本地假数据;业务规则不依赖某个具体 Widget;底层插件不散落在各个页面里。

需求到任务的拆解

一个功能不要直接拆成“写页面”。更可靠的拆法是从业务路径拆到工程任务。

业务点工程任务
用户登录表单校验、登录接口、token 存储、刷新 token、退出登录
待办列表列表页面、分页、下拉刷新、空状态、错误态、缓存读取
待办详情路由参数、详情接口、无权限、数据不存在、操作按钮状态
新建待办表单状态、草稿、重复提交、接口错误、创建成功后列表刷新
离线编辑本地队列、同步状态、冲突处理、失败重试

每个功能都应该补齐四件事:页面表现、数据来源、异常分支、验证方式。只写成功路径,会让项目在联调、测试和上线阶段集中暴露问题。

状态设计

每个页面至少定义清楚四类状态:

状态页面表现
Loading骨架屏或加载指示
Success正常内容
Empty空数据说明和下一步动作
Error错误原因、重试入口、必要日志

复杂页面可以进一步拆出 refreshingloadingMoreofflinepartialError 等状态。

一个页面状态可以这样表达:

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 是数据边界。页面不直接知道数据来自网络还是本地:

  1. 先从缓存读取可用数据。
  2. 同时发起网络刷新。
  3. 网络成功后写回缓存。
  4. 网络失败时展示可恢复错误。
  5. 离线写入进入同步队列。

推荐的数据流:

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 analyzeflutter test 能通过。
  • Android/iOS 发布配置有文档记录。
  • 线上崩溃、日志、性能和关键埋点有接入方案。
  • 有交接文档,包含启动命令、环境变量、发布步骤、常见问题和回滚方式。
别急,先让缓存热一下。