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、Bloc buildWhen、拆 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. 演进路径

  1. 单 App + 特性目录 — Lint 分层;摸清领域;
  2. 抽出 design_system + core — 停止复制 UI;
  3. 热特性独立成包 — 优先 auth、messaging;
  4. 引入 *_api — 第二团队要集成时;
  5. 薄壳 App — 仅路由 + DI;特性按需独立版本。

在 归属或编译时间 成为瓶颈时再拆包——非 LOC 过线就拆。

16. 反模式

  • 全 App 仅 按技术层 分根目录;
  • 特性 A import 特性 B 的 Screen;
  • 上帝 AppState 塞用户、购物车、聊天、主题;
  • 业务规则写在 Widget 或 presentation 的 JSON Map;
  • 层间 无类型 Map<String, dynamic> 乱传;
  • 每特性复制 Dio、拦截器不一致;
  • Golden/设计测 上线后再说;
  • 只 profile Debug;
  • 本地跑不通就 微前端 化;
  • 平台通道 无门面、无 Fake。

小结

大型 Flutter 项目是 模块化产品工程:

  1. 有规则的包图 — api / impl、禁止 impl 横向依赖;
  2. 特性内分层 — presentation → domain → data;用例居中;
  3. 状态分作用域 — 本地、特性、会话;非单一全局袋;
  4. 路由归壳 — URL、ID、深链;
  5. Repository + 离线策略 — DTO/Domain 分离;
  6. 平台门面 — 可测、类型化、集中;
  7. Design System — Token 与组件,非杂物抽屉;
  8. 按包测试 + CI — 接缝 Fake;Release 剖析;
  9. 随团队边界演进分包 — 非过早分布式表演。

Flutter 的跨端交互质量是优势;依赖纪律 决定人头与特性增多后,这一优势能否保住。