# 08. 多环境构建 ## 决策 App 侧维护 **3 个 flavor:`dev` / `uat` / `prod`**,环境划分与现有后端 CI/CD(见 `gitlab-cicd-azure-deployment-diagram.drawio` 里的 Dev/UAT/Prod Azure 环境)保持一致命名,三个 flavor 各自对应不同的 API 地址、应用图标/名称、包名后缀,可在同一台设备上同时安装、互不覆盖。CI 沿用现有 GitLab CI(Runner 与后端共用),但产物是 App 二进制(apk/ipa),分发渠道与后端的 ACR/Azure App Hosting 不同。 ## Flavor 划分规则 | Flavor | Application ID / Bundle ID | API 目标 | 分发渠道 | |---|---|---|---| | `dev` | `com.conti.retail.dev` | Dev 环境(对应后端 Dev) | Firebase App Distribution(内部测试) | | `uat` | `com.conti.retail.uat` | UAT 环境(对应后端 UAT) | Firebase App Distribution(验收测试) | | `prod` | `com.conti.retail` | Prod 环境(对应后端 Prod) | App Store Connect / Google Play(生产发布) | ## 使用规则 - 每个 flavor 对应一个独立的 Dart 入口文件(`main_dev.dart`/`main_uat.dart`/`main_prod.dart`),三者都只是设置好环境标识后调用同一个共享的 `bootstrap()` 启动函数,不允许在入口文件里写业务逻辑分支。 - 环境相关的可变配置(API base URL、是否开启日志等,见 [05-networking.md](./05-networking.md) 的 `appEnvProvider`)通过 `--dart-define-from-file=env/{flavor}.json` 注入,不写死在代码里、也不用 `if (flavor == 'dev')` 这种运行时字符串判断来分支配置。 - `env/*.json` 只包含非敏感配置(API 地址等);密钥类配置(如第三方 SDK App Key)通过 CI 变量在构建时注入,不提交进仓库。 - Android 侧用 Gradle `productFlavors` 区分 `applicationId`/图标/`versionNameSuffix`;iOS 侧用对应的 xcconfig + Scheme 区分 `Bundle Identifier`/图标;两端 flavor 名称必须完全一致(`dev`/`uat`/`prod`),不允许两端用不同命名。 - CI 流水线阶段固定为:`melos run analyze` → `melos run test` → 按 flavor `flutter build apk/ipa --flavor {flavor} --dart-define-from-file=env/{flavor}.json` → 上传对应分发渠道。`prod` flavor 的构建触发条件是打 tag,不是每次 push 都触发(避免误发生产包)。 ## 参考链接 - [Flutter 官方 Flavors 文档](https://docs.flutter.dev/deployment/flavors) - [--dart-define-from-file 官方说明](https://docs.flutter.dev/deployment/flavors#configuration-approaches) - [Firebase App Distribution](https://firebase.google.com/docs/app-distribution) ## 附录:Flavor 是什么,日常怎么用 给还没接触过多环境构建方式的同学看的入门说明。 ### 要解决的问题 一个 App 通常需要同时存在"开发中还没上线的版本"和"已经上线的正式版本",测试期间还需要一个给验收测试用的版本——这三个版本理想情况下要能**同时装在同一台测试手机上**,方便对比测试,而不是每次切换环境都要卸载重装。同时它们各自要打到不同的后端地址(Dev/UAT/Prod API),不能一个 apk 通过运行时开关切换后端就完事——因为如果只用运行时环境变量,三个环境包的 `applicationId`/`Bundle ID` 完全一样,装第二个会直接覆盖第一个。 **Flavor** 是 Android(Gradle `productFlavors`,历史悠久的原生概念)和 iOS(Xcode Build Configuration/Scheme)本来就有的机制:在同一份代码基础上,用不同的编译配置产出`applicationId`/图标/名称都不同的多个安装包。Flutter 从工具链层面(`flutter build --flavor xxx`)把两端的 flavor 机制包装成统一的命令行接口。 ### 核心概念 1. **Android `productFlavors`**:在 `android/app/build.gradle` 里声明多套 `applicationId`/`versionNameSuffix`/资源目录,编译时用 `--flavor` 选择其中一套。 2. **iOS Scheme + xcconfig**:iOS 没有 Gradle 那样的单文件配置,而是通过 Xcode 里多个 Build Configuration(对应不同的 `.xcconfig` 文件设置 Bundle ID 等)+ 多个 Scheme 组合实现同样的效果,`flutter build ipa --flavor xxx` 背后就是选中同名 Scheme。 3. **Dart 入口文件(`main_xxx.dart`)**:flavor 决定的是"编译出什么样的原生外壳",Dart 代码本身默认只有一个 `main.dart` 入口——项目约定用多个入口文件对应各 flavor,让每个 flavor 能设置不同的启动参数(比如传给 `bootstrap()` 一个环境枚举)。 4. **`--dart-define-from-file`**:flavor 解决的是原生层面的差异(图标、包名),但 API 地址这类 Dart 侧读取的配置,用编译期注入的 JSON 文件解决,避免打包进一个写死 `http://dev-api...` 的字符串常量。 ### 使用示例 Android 侧 flavor 声明(`android/app/build.gradle`): ```gradle android { flavorDimensions "env" productFlavors { dev { dimension "env" applicationId "com.conti.retail.dev" versionNameSuffix "-dev" } uat { dimension "env" applicationId "com.conti.retail.uat" versionNameSuffix "-uat" } prod { dimension "env" applicationId "com.conti.retail" } } } ``` 环境配置文件(`env/dev.json`,非敏感部分): ```json { "API_BASE_URL": "https://dev-api.conti-retail.com", "ENABLE_LOG": true } ``` 共享启动入口 + 各 flavor 的 Dart 入口文件: ```dart // lib/bootstrap.dart —— 三个 flavor 共用的启动逻辑 Future bootstrap(AppEnv env) async { runApp(ProviderScope( overrides: [appEnvProvider.overrideWithValue(env)], child: const App(), )); } ``` ```dart // lib/main_dev.dart void main() => bootstrap(AppEnv.fromDartDefine(name: 'dev')); ``` 构建命令: ```bash flutter build apk \ --flavor dev \ --target lib/main_dev.dart \ --dart-define-from-file=env/dev.json ``` GitLab CI 片段(衔接现有 Runner,产物走 Firebase App Distribution 而非后端用的 ACR): ```yaml build_dev: stage: build script: - melos run analyze - melos run test - flutter build apk --flavor dev --target lib/main_dev.dart --dart-define-from-file=env/dev.json - firebase appdistribution:distribute build/app/outputs/.../dev/release/app-dev-release.apk rules: - if: '$CI_COMMIT_BRANCH == "develop"' build_prod: stage: build script: - flutter build appbundle --flavor prod --target lib/main_prod.dart --dart-define-from-file=env/prod.json rules: - if: '$CI_COMMIT_TAG' # 只在打 tag 时触发,避免误发生产包 ```