Files
conti-docs/flutter-app/01-project-structure.md

16 KiB
Raw Permalink Blame History

01. 工程结构 / 分包策略

决策

使用 Melos monorepo,按 feature 拆分成独立 Dart package,而不是单一 Flutter package 内部用文件夹分层。

包结构总览

conti-app/
  pubspec.yaml              # 根 workspace 配置(melos 8.x 不再有独立 melos.yaml,见下文)
  .fvmrc                    # 锁定 Flutter SDK 版本
  analysis_options.yaml     # 全仓库共享 lint 规则
  app/                      # 壳工程:唯一的 Flutter application,负责路由汇总、DI 装配、编译出 ipa/apk
  packages/
    core_ui/                 # 通用组件、主题、设计 token
    core_network/            # dio 封装、拦截器、统一异常、ApiResult 解包
    core_storage/            # 本地存储抽象(Drift + shared_preferences 封装)
    core_auth/                # 登录态、token 管理、secure storage、门店上下文
    core_router/              # 路由聚合、公共 route guard、动态菜单映射
    core_webview/             # F6 H5 容器 + JSBridge(见 10-webview-h5.md
    core_analytics/           # 埋点统一 API(见 13-observability-analytics.md
    core_logging/             # 日志规范、脱敏、崩溃上报接入
    feature_auth/             # 登录、验证码、用户协议与隐私政策
    feature_home/             # 首页工作台:动态菜单、待办、预警、公告、促销位
    feature_store_mgmt/       # 店铺管理:基础信息、服务信息、执照、人员管理
    feature_sales/            # 销售流程:客户查询、历史工单、商机(H5 承载的部分走 core_webview
    feature_purchase/         # 采购:产品查询、购物车、结算、订单、收货
    feature_inventory/        # 库存:明细、安全库存、盘点、DOT
    feature_analytics/        # 经营分析:对账单、核销收入、返利、报表
    feature_profile/          # 个人中心:地址、热线、客服
    feature_scan/             # 扫码业务入口(VIN/车牌/二维码/条码 → 分发到对应业务)
    native_scan/              # 原生插件包:扫码 + VIN 识别(车牌走云端 OCR,不在这里)
    native_media/             # 相机、相册、文件选择/上传
    native_device/            # 拨号、设备信息、权限申请
    native_crash/             # Bugly 崩溃上报的原生桥(见 13-observability-analytics.md

包清单按 PRD 第 4 章 的模块划分列出,实际开工时按迭代顺序逐个建,不需要一次性全建出来。

依赖规则(编译期强制边界,是这套结构的核心价值)

  • app 可以依赖所有 core_*feature_*
  • feature_* 只能依赖 core_*native_*不能相互依赖(feature_purchasepubspec.yaml 里不允许出现 feature_inventory 的 path dependency)。
  • core_* 可以依赖 native_*core_webview 的 JSBridge 需要调起扫码/相机/上传)。
  • native_* 只依赖 Flutter SDK 和 Pigeon 生成的代码,不依赖任何 core_* / feature_*——保证原生插件包可以脱离业务单独编译、单独测试(详见 07-native-integration.md)。

core_* 之间原则上不互相依赖,允许的例外只有下面三条,多一条都要走评审:

允许的依赖 原因
core_networkcore_auth 取 token 附加到请求头、401 时触发刷新
core_routercore_auth 路由 redirect 里判断登录态(见 04-routing.md
core_webviewcore_auth H5 换票需要当前登录态与门店上下文(见 10-webview-h5.md

两条容易踩的反向约束,必须记住:

  • core_auth 不依赖 core_networkcore_auth 要发 refresh 请求,如果依赖 core_network 就和上表第一行构成循环依赖。做法是:core_auth 直接依赖 dio 包,内部自建一个不挂任何拦截器的裸 Dio 实例专门用于刷新——这同时也避免了"刷新请求本身被 AuthInterceptor 拦截 → 401 → 再刷新"的递归(见 05-networking.md)。
  • core_auth 不依赖 core_storage。token / refresh token 走 flutter_secure_storage,这个依赖core_auth 独占core_storage 只负责 Drift 和 shared_preferences(见 06-local-storage.md)。这样划分是为了不让 core_* 之间再多一条依赖边。

feature_* 不直接依赖 go_router,路由相关类型由 core_router 统一 re-exportexport 'package:go_router/go_router.dart';),这样将来换路由库时只有 core_router 一个包要改。

这些规则由 Dart 的包依赖机制物理强制feature_a 根本 import 不到 feature_b 的任何符号,不是靠代码规范或 review 口头约束。

Feature 间通信怎么处理

这是最容易被绕开、也是这套边界能否守住的关键点,必须写清楚合法方式:

  1. 路由跳转 + 可序列化参数(多数场景)——比如从 feature_home 跳到 feature_purchase,通过 core_router 声明的路径 + query/extra 参数传递,不直接引用对方的 Dart 类型。
  2. 通过 core_* 定义的抽象接口 + DI 注册实现——真正需要跨 feature 拿数据或发通知的场景(比如切换门店后要清空购物车),在某个 core_* 包里定义接口,各 feature 各自实现并在 app 层注册,调用方只依赖 core_* 里的抽象类型(门店切换的级联失效见 11-store-context-and-session.md)。

不允许的做法:任何 feature_*pubspec.yaml 里直接 path dependency 另一个 feature_*,哪怕只是想复用一个 widget——这种情况应该把这个 widget 提到 core_ui

命名规范

  • core_xxx:基础设施层,不含具体业务逻辑。
  • feature_xxx:对应一个业务域(多数是原来的某个小程序,也有全新的,如首页工作台)。
  • native_xxx:原生能力插件包,首版含 android/ios/ 两套原生实现目录(OHOS 不在首版范围,见 07-native-integration.md)。

SDK 版本基线

版本 说明
Flutter 3.44.9 FVM 锁定,仓库根目录提交 .fvmrc
Dart 随 Flutter 3.44.9 附带(3.12.x 具体号以 flutter --version 实测为准;environment: sdk: ^3.12.0 对整个 3.12.x 都成立

为什么不跟最新 stable3.47.0 / Dart 3.13.02026-08-12 发布):鸿蒙(OpenHarmony)的 Flutter 分支适配落后于官方 stable 一段时间,虽然 OHOS 不在首版范围(见 07-native-integration.md 的「OHOS 后续演进」),但 SDK 基线要为后续接 OHOS 留出兼容窗口,所以刻意停在 3.44.9 而不是追最新。这条约束在决定升级 Flutter 版本时必须重新评估,不要因为"新版本有新特性"就单方面升。

为什么必须用 FVM 锁monorepo 里各人本地 Flutter 版本不一致,会导致同一份代码有人 flutter analyze 过、有人不过,生成代码(build_runner 产物)也可能不一致——这类问题排查成本远高于装一次 FVM。CI 也用 .fvmrc 里的版本,保证本地和流水线一致。

// .fvmrc
{ "flutter": "3.44.9" }

Melos 配置示例(8.x,基于 Dart Pub Workspaces

Melos 7.0 起改用 Dart 官方原生的 Pub Workspaces 机制,不再有独立的 melos.yaml 文件,配置写进根目录 pubspec.yaml;每个子包的 pubspec.yaml 需要加 resolution: workspace

两个不同的 SDK 下限,别搞混:

  • Pub Workspaces 机制本身要求 Dart SDK ≥ 3.6.0
  • melos 8.2.2 这个工具自己要求 Dart SDK ^3.9.0

我们的基线(Dart 3.12.x)两条都满足。

根目录 pubspec.yaml

name: conti_app
publish_to: none
environment:
  sdk: ^3.12.0

workspace:
  - app
  - packages/core_ui
  - packages/core_network
  - packages/core_storage
  - packages/core_auth
  - packages/core_router
  - packages/core_webview
  - packages/core_analytics
  - packages/core_logging
  - packages/feature_auth
  - packages/feature_home
  - packages/feature_purchase
  - packages/native_scan
  # ... 其余包按实际建包进度追加

dev_dependencies:
  melos: ^8.2.2

melos:
  scripts:
    analyze:
      run: melos exec --fail-fast -- flutter analyze
    test:
      # --dir-exists=test 跳过还没有测试目录的包(如新建的 native_*),
      # 否则批量命令会因为「找不到 test 目录」整体失败
      run: melos exec --dir-exists=test --fail-fast -- flutter test
    format:
      run: melos exec -- dart format --set-exit-if-changed .
    gen:
      # 代码生成:riverpod_generator / drift_dev / json_serializable
      run: melos exec --depends-on=build_runner -- dart run build_runner build --delete-conflicting-outputs
    pigeon:
      # 原生接口生成,见 07-native-integration.md
      run: melos exec --scope="native_*" -- dart run pigeon --input pigeons/

每个子包(比如 packages/feature_purchase/pubspec.yaml):

name: feature_purchase
resolution: workspace

dependencies:
  core_ui:
    path: ../core_ui
  core_network:
    path: ../core_network
  core_router:
    path: ../core_router

共享 lint 配置

根目录一份 analysis_options.yaml,各子包 include 它,不允许各包自己维护一套规则(选型与具体规则见 14-conventions-and-ci-gates.md):

# packages/feature_purchase/analysis_options.yaml
include: ../../analysis_options.yaml

用到 custom_lintriverpod_lint 依赖它)的包,需要各自在 dev_dependencies 里加 custom_lint,并在自己的 analysis_options.yaml 里启用 custom_lint 插件——custom_lint 是按包运行的,不能只在根目录配一次(见 03-state-management.md)。

新增 feature 包的标准脚手架

feature_xxx/
  pubspec.yaml         # resolution: workspace + 依赖 core_ui / core_network / core_router 等,不依赖其他 feature
  lib/
    feature_xxx.dart    # 唯一对外导出文件(barrel file):只暴露路由注册函数和必要的 public widget
    src/
      presentation/
      domain/            # 可选,见下方分层规范文档
      data/
  test/

src/ 目录下的内容视为包内私有实现,只有 feature_xxx.dart 这一个文件是对外契约——这条靠 code review 检查,Dart 语言本身没有强制的 package-private 关键字。domain/ 目录的取舍规则详见 02-layering.md

版本管理

不发布到 pub.dev,全部用 melos 的 path dependency,包版本号跟随 app 的整体版本号统一管理(fixed versioning),不做 melos 的 independent versioning——没有对外发布需求,独立版本号只会增加维护负担。

附录:Melos 是什么,日常怎么用

给没接触过 Dart 多包仓库工具的同学看的入门说明。

要解决的问题

Dart 官方的包管理工具 pub 天生只认"一个 pubspec.yaml = 一个包"。如果要在同一个 git 仓库里维护多个互相依赖的私有包(比如 app 依赖 feature_scanfeature_scan 依赖 core_network),原生 pub 只支持手动在每个包的 pubspec.yaml 里写 path: ../../packages/core_network 这种相对路径依赖——能跑,但没有任何批量操作能力:想给所有包统一跑一次 flutter analyzeflutter test,或者统一升级某个第三方库版本,都得一个包一个包手动进去执行。

Melos 就是给这种多包仓库提供批量管理能力的工具,类似 JS 生态里的 Lerna/Nx,只不过是 Dart/Flutter 版本。它不改变 Dart 语言或 pub 本身的机制,只是在多个包外面包一层"批处理脚本 + 配置"。

核心概念

  1. 根目录 pubspec.yaml 里的 workspace: 字段 + melos: 配置块8.x 版本不再有独立的 melos.yaml 文件(7.0 之前是独立文件,现已合并进 Dart 官方原生的 Pub Workspaces 机制)。workspace: 列出所有子包路径,melos: 块下的 scripts: 定义可复用脚本(见上文示例)。
  2. melos bootstrap(简写 melos bs):一键解析 workspace 内所有包之间的依赖关系。在 8.x 的 Pub Workspaces 模式下,它的效果约等于"在仓库根目录跑一次 flutter pub get + 校验各包 resolution: workspace 配置是否正确"——包间链接由 pub 原生的 workspace 机制完成,不再生成 pubspec_overrides.yaml(那是 7.0 之前的实现方式)。新人拉下代码后第一步永远是跑这个命令
  3. melos exec:在每一个包目录下依次/并行执行同一条命令,比如 melos exec -- flutter test 就是把所有包都跑一遍测试,替代手动 cd packages/feature_purchase && flutter test && cd ../feature_inventory && ...。常用过滤参数:--scope(只跑匹配名字的包)、--dir-exists=test(只跑有测试目录的包)、--fail-fast(有一个包失败就停)。
  4. melos run <script-name>:调用根目录 pubspec.yamlmelos: scripts: 下预定义的脚本别名(比如上文的 melos run test),团队里统一敲固定命令,不用记 exec 的完整写法。

日常开发流程(拿本仓库举例)

# 0. 一次性:安装 fvm 并装上基线版本的 Flutter
dart pub global activate fvm
fvm install          # 读 .fvmrc,装 3.44.9
fvm flutter --version

# 1. 第一次拉代码,或者别人加了新包/新依赖之后
melos bootstrap

# 2. 正常改代码,比如在 feature_purchase 里改一个页面
cd packages/feature_purchase
flutter run     # 这一步跟平时写单个 Flutter 项目完全一样,感觉不到 melos 的存在

# 3. 改了带注解的代码(Riverpod / Drift / json_serializable)之后
melos run gen

# 4. 提交前,跑一遍全仓库检查
melos run analyze
melos run test

# 5. 新增了 feature 包,或者某个包新增了对另一个包的依赖之后
melos bootstrap   # 重新解析依赖关系

关键体感:平时在某一个包里写代码、flutter run、热重载,跟没有 melos 时完全一样——melos 只在"跨包操作"(装依赖、批量测试、批量分析)时才会用到,不侵入日常单包开发的手感。

常见的坑

  • 加了新包,或改了某个包的依赖之后忘记跑 melos bootstrap,会出现"明明加了依赖但 import 不到"的报错——看到这个报错先跑一遍 bootstrap 再排查。
  • 8.x 基于 Pub Workspaces 后,正常的包间链接不再依赖 pubspec_overrides.yaml(这是 7.0 之前版本的机制);只有配置了额外的 dependencyOverridePaths(用于覆盖外部第三方依赖,不是本仓库包之间的常规场景)时才会生成这个文件。如果看到这个文件出现却不记得配置过覆盖路径,说明配置可能有误,需要检查。

安装

dart pub global activate melos

全局命令,装一次即可,不需要每个项目单独安装。

参考链接