Flutter 在第一年迭代很快,第三年往往变痛:特性互相 import、全局状态 掩盖数据流、平台调用 散落在 Widget 里。规模问题通常不是 Riverpod 还是 Bloc——而是 归属、依赖方向 与 接缝,让团队能增量发布而不做全库回归。
下文把大型 Flutter 代码库当作 产品平台:如何分包、限定状态、跨特性路由、在合适层测试,并从单仓演进到多贡献者——而不是过早拆成本地都跑不起来的「微前端」。
1. 无边界的代码库长什么样
| 症状 | 根因 | 代价 |
|------|------|------|
| 改设置却弄坏 checkout | 特性直接 import 另一特性 UI | 回归狩猎 |
| 「用全局 Provider 就行」 | 无状态作用域规则 | 隐蔽 rebuild、测试 flaky |
| 改一行 CI 40 分钟 | 无包图;全量重编 | 速度崩盘 |
| 仅三星机平台崩溃 | Widget 内 MethodChannel | 原生接缝不可测 |
| Web 与 mobile 悄然分叉 | 表现层 #ifdef | 双倍维护 |
依赖图若是一团乱麻,换框架救不了。先画边界。
2. 包拓扑:谁依赖谁
超过约 50k LOC 或 3+ 小队 时,宜用 Monorepo 多包(Melos 或 path 依赖):
apps/
mobile/ # 薄壳:启动、路由、DI 根
packages/
design_system/ # Token、组件、主题
core/ # 日志、分析、环境、共享错误
feature_auth/
auth_api/ # 仅公开模型 + 接口
auth_impl/ # UI + data + domain(可再拆)
feature_messaging/
messaging_api/
messaging_impl/
feature_settings/
...
依赖规则(CI 用 dependency_validator 或脚本强制):
| 包 | 可依赖 | 不可依赖 |
|----|--------|----------|
| *_api | core、少量其他 *_api | 任何 *_impl、他特性 Widget |
| *_impl | 自身 *_api、design_system、core | 其他特性 *_impl |
| design_system | core(尽量少) | 特性包 |
| apps/mobile | 各 *_impl、路由胶水 | — |
跨特性需求(如「订单详情进聊天」)走 api 契约——接口、ID、深链路由——而非 import 对方 Screen。
flowchart LR
App[mobile shell]
FA[feature_a_impl]
FB[feature_b_impl]
AA[feature_a_api]
AB[feature_b_api]
DS[design_system]
Core[core]
App --> FA
App --> FB
FA --> AA
FB --> AB
FA --> DS
FB --> DS
FA --> Core
FB --> Core
AA --> Core
AB --> Core
3. 特性内部分层
每个 *_impl 内保持 presentation → domain → data 依赖方向:
feature_x/
presentation/ # Widget、ViewModel、Notifier
domain/ # 实体、用例、Repository 接口
data/ # DTO、API、本地库、Repository 实现
| 层 | 放什么 | 避免 |
|----|--------|------|
| Presentation | Widget、状态机、路由参数映射 | SQL、JSON 字段名、MethodChannel |
| Domain | 业务规则、纯 Dart 实体 | BuildContext、Flutter UI import |
| Data | Dio/Drift、DTO↔domain 映射 | Widget rebuild 逻辑 |
用例(单职责类/函数)保持 Widget 薄:LoadThread、SendMessage、MarkRead——编排 Repository,暴露明确失败类型。
4. 状态:先定作用域,再选框架
Riverpod、Bloc、MobX 均可——规则比品牌重要:
| 作用域 | 示例 | 生命周期 |
|--------|------|----------|
| Ephemeral / 本地 | 动画、表单草稿、滚动位置 | StatefulWidget、HookWidget |
| 特性级 | 收件箱列表、撰写流程 | 特性路由子树内 Provider |
| 应用会话 | 登录态、语言、主题 | 根 Provider;面积尽量小 |
| 持久化 | 用户偏好、离线缓存 | Repository + Domain;非裸全局单例 |
宜尽早写进规范的规则:
- 特性不得读取他特性 私有 Notifier;
- 副作用(API、导航)放在 Notifier/Bloc,不在
build(); - 异步显式
loading / data / error——避免布尔组合地狱; - 路由 pop 时取消进行中的请求、释放订阅。
全局化一切 → 每次改动都像全 App 回归。在路由级限定 Provider(ProviderScope override、特性壳内 BlocProvider)。
5. 导航即公开契约
由 App 壳持有 声明式路由(go_router、auto_route):
- 路由即 URL —
/orders/:id、/chat/:threadId;支持深链与 Web; - 特性注册路由 — 插件模式或生成路由表;
- 跨特性导航 传 ID 与意图,非 Widget 实例:
context.go('/chat/$threadId'),而非Navigator.push(OtherFeatureScreen(...)); - 鉴权重定向 集中在 router redirect;
- 嵌套导航 — Tab 用 shell route;栈用内层 Navigator。
路由参数解析留在 presentation mapper;Domain 接收 typed ID / 值对象。
6. 数据层:Repository 与离线
6.1 Repository 模式
Widget 与 Notifier 依赖 Domain 内 接口:
abstract class MessageRepository {
Stream<List<Message>> watchThread(ThreadId id);
Future<Result<void>> send(SendCommand cmd);
}
实现组合 远程 + 本地;Domain 不见 HTTP 状态码。
6.2 DTO 与 Domain
- DTO 对齐 API JSON(
freezed+json_serializable); - Mapper 在 data 边界;
- Domain 模型 在 API v2 增字段时仍稳定。
6.3 网络
- 单一
Dio+ 拦截器:刷新 Token、Trace ID、重试策略; - 按接口类型设超时;
- 变更类请求在服务端支持时使用 幂等键。
6.4 本地存储
- Drift/Isar/Hive 做结构化缓存;按实体定义 TTL 与失效;
- 离线优先 仅在产品需要时做——冲突策略(LWW、服务端胜出)须写清;
- Migration 像后端 Schema 一样版本化。
7. 平台通道:门面,而非 Widget 直调
推送、生物识别、后台任务、BLE 等跨 不可靠边界:
| 做法 | 原因 |
|------|------|
| Dart 门面接口(PushNotifications、SecureStorage) | Widget 可测 |
| Pigeon 或类型化封装 | 少字符串协议错误 |
| 每域 单一 channel 名 | 避免冲突 |
| 平台失败映射为 Result/领域错误 | 统一处理 |
| 单元/Widget 测用 Fake | CI 无需模拟器 |
| 集成测 跑真通道 | nightly 或发版前 |
勿在 build() 里散落 MethodChannel('foo')。Native 侧归平台同学;Dart 侧归门面契约。
8. Design System 当作产品
design_system/ 不是「公共 Widget 文件夹」:
- Token — 色、间距、字阶、圆角(浅/深/高对比);
- 组件 — 按钮、输入、Sheet,文档化 disabled/loading/error;
- 无障碍 — 语义、对比度、最小点击区域内置;
- 无业务逻辑 — 原子组件内不调 API、不读特性开关;
- 版本化 — 破坏性视觉变更走设计评审 + Golden 更新。
特性用 design system + 本特性分子组件拼屏。勿每特性 fork 一个按钮。
9. 大型项目的测试金字塔
| 层 | 目标 | 工具 |
|----|------|------|
| 单元 | 用例、Mapper、纯 Domain | test |
| Widget | 假 Repository 驱动的屏 | flutter_test、Provider override |
| Golden | 设计系统视觉回归 | golden_toolkit / Alchemist |
| 集成 | 端到端、真通道 | integration_test、设备 lab |
在 *_api 边界做 契约测试:Fake 同时服务消费方与生产方测试。
CI 应 按受影响包 运行(Melos exec --scope 或构建图)——非每次改文档跑全矩阵。
10. 性能:以 Release 为准
Debug 会骗人。生产仍成立的习惯:
- 稳定值尽量
const构造; - 列表虚拟化 —
ListView.builder、Sliver;避免无界Column堆子节点; - 图片 — 限制解码尺寸、缓存宽高、
RepaintBoundary隔离重绘; - 重活 — JSON 解析、图像、加密用
compute()/ Isolate; - Rebuild 控制 — Riverpod
select、BlocbuildWhen、拆 Widget; - Shader jank — Release 剖析;目标机上看 Impeller vs Skia;
- 启动 — 非关键初始化延后;路由与 SDK 懒注册。
设预算:首帧、滚动 jank 比例、包体大小——可行则进 CI。
11. 环境、Flavor 与开关
- Flavor —
dev/staging/prod,不同包名、API、日志级别; --dart-define或 env 文件 注入配置——密钥不进仓库;- 特性开关 — 远程 + 代码默认(离线可运行);
- 壳读取开关,经 DI 注入特性——非处处直连 SDK 单例。
12. 代码生成与一致性
规模上来,手写样板会漂移:
freezed— UI 状态联合类型、不可变模型;json_serializable— DTO;retrofit等 — API 定义;drift— 生成 SQL;- 路由 builder — 类型安全路径。
用 特性模板(melos run create_feature)统一新模式,避免第三套写法。
13. 错误、日志与可观测
- Domain 错误 — 密封类(
NetworkFailure、Unauthorized等); - Presentation 映射为用户文案 + 重试;
- 全局处理 — 日志 + Crashlytics/Sentry,带 breadcrumb;
- 分析事件 — 在
core定义、一处实现;特性发 typed event; - PII 策略 — 日志无裸 Token;拦截器 scrub。
Dio 拦截器注入 X-Trace-Id,与后端链路关联。
14. 多平台接缝
一套代码 ≠ 行为完全一致:
| 关注点 | Mobile | Web | Desktop | |----------|--------|-----|---------| | 导航 | 深链、返回手势 | 地址栏、刷新 | 窗口缩放 | | 存储 | Keychain/Keystore | localStorage 限制 | 文件路径 | | 后台 | OS 限制 | Tab 冻结 | 因平台而异 |
门面层 做 Platform.isX,勿在 Widget 里散落。平台实现可用 条件 import(stub/io/web)。
Web:关注 包体、推迟 WASM/重库、测 Safari 而非只测 Chrome。
15. 演进路径
- 单 App + 特性目录 — Lint 分层;摸清领域;
- 抽出
design_system+core— 停止复制 UI; - 热特性独立成包 — 优先 auth、messaging;
- 引入
*_api— 第二团队要集成时; - 薄壳 App — 仅路由 + DI;特性按需独立版本。
在 归属或编译时间 成为瓶颈时再拆包——非 LOC 过线就拆。
16. 反模式
- 全 App 仅 按技术层 分根目录;
- 特性 A import 特性 B 的 Screen;
- 上帝 AppState 塞用户、购物车、聊天、主题;
- 业务规则写在 Widget 或 presentation 的 JSON Map;
- 层间 无类型
Map<String, dynamic>乱传; - 每特性复制 Dio、拦截器不一致;
- Golden/设计测 上线后再说;
- 只 profile Debug;
- 本地跑不通就 微前端 化;
- 平台通道 无门面、无 Fake。
小结
大型 Flutter 项目是 模块化产品工程:
- 有规则的包图 —
api/impl、禁止 impl 横向依赖; - 特性内分层 — presentation → domain → data;用例居中;
- 状态分作用域 — 本地、特性、会话;非单一全局袋;
- 路由归壳 — URL、ID、深链;
- Repository + 离线策略 — DTO/Domain 分离;
- 平台门面 — 可测、类型化、集中;
- Design System — Token 与组件,非杂物抽屉;
- 按包测试 + CI — 接缝 Fake;Release 剖析;
- 随团队边界演进分包 — 非过早分布式表演。
Flutter 的跨端交互质量是优势;依赖纪律 决定人头与特性增多后,这一优势能否保住。