Files
conti-docs/08-build-flavors.md
T

6.5 KiB
Raw Blame History

08. 多环境构建

决策

App 侧维护 3 个 flavordev / 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.mdappEnvProvider)通过 --dart-define-from-file=env/{flavor}.json 注入,不写死在代码里、也不用 if (flavor == 'dev') 这种运行时字符串判断来分支配置。
  • env/*.json 只包含非敏感配置(API 地址等);密钥类配置(如第三方 SDK App Key)通过 CI 变量在构建时注入,不提交进仓库。
  • Android 侧用 Gradle productFlavors 区分 applicationId/图标/versionNameSuffixiOS 侧用对应的 xcconfig + Scheme 区分 Bundle Identifier/图标;两端 flavor 名称必须完全一致(dev/uat/prod),不允许两端用不同命名。
  • CI 流水线阶段固定为:melos run analyzemelos run test → 按 flavor flutter build apk/ipa --flavor {flavor} --dart-define-from-file=env/{flavor}.json → 上传对应分发渠道。prod flavor 的构建触发条件是打 tag,不是每次 push 都触发(避免误发生产包)。

参考链接

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

给还没接触过多环境构建方式的同学看的入门说明。

要解决的问题

一个 App 通常需要同时存在"开发中还没上线的版本"和"已经上线的正式版本",测试期间还需要一个给验收测试用的版本——这三个版本理想情况下要能同时装在同一台测试手机上,方便对比测试,而不是每次切换环境都要卸载重装。同时它们各自要打到不同的后端地址(Dev/UAT/Prod API),不能一个 apk 通过运行时开关切换后端就完事——因为如果只用运行时环境变量,三个环境包的 applicationId/Bundle ID 完全一样,装第二个会直接覆盖第一个。

Flavor 是 AndroidGradle productFlavors,历史悠久的原生概念)和 iOSXcode Build Configuration/Scheme)本来就有的机制:在同一份代码基础上,用不同的编译配置产出applicationId/图标/名称都不同的多个安装包。Flutter 从工具链层面(flutter build --flavor xxx)把两端的 flavor 机制包装成统一的命令行接口。

核心概念

  1. Android productFlavors:在 android/app/build.gradle 里声明多套 applicationId/versionNameSuffix/资源目录,编译时用 --flavor 选择其中一套。
  2. iOS Scheme + xcconfigiOS 没有 Gradle 那样的单文件配置,而是通过 Xcode 里多个 Build Configuration(对应不同的 .xcconfig 文件设置 Bundle ID 等)+ 多个 Scheme 组合实现同样的效果,flutter build ipa --flavor xxx 背后就是选中同名 Scheme。
  3. Dart 入口文件(main_xxx.dartflavor 决定的是"编译出什么样的原生外壳",Dart 代码本身默认只有一个 main.dart 入口——项目约定用多个入口文件对应各 flavor,让每个 flavor 能设置不同的启动参数(比如传给 bootstrap() 一个环境枚举)。
  4. --dart-define-from-file:flavor 解决的是原生层面的差异(图标、包名),但 API 地址这类 Dart 侧读取的配置,用编译期注入的 JSON 文件解决,避免打包进一个写死 http://dev-api... 的字符串常量。

使用示例

Android 侧 flavor 声明(android/app/build.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,非敏感部分):

{
  "API_BASE_URL": "https://dev-api.conti-retail.com",
  "ENABLE_LOG": true
}

共享启动入口 + 各 flavor 的 Dart 入口文件:

// lib/bootstrap.dart —— 三个 flavor 共用的启动逻辑
Future<void> bootstrap(AppEnv env) async {
  runApp(ProviderScope(
    overrides: [appEnvProvider.overrideWithValue(env)],
    child: const App(),
  ));
}
// lib/main_dev.dart
void main() => bootstrap(AppEnv.fromDartDefine(name: 'dev'));

构建命令:

flutter build apk \
  --flavor dev \
  --target lib/main_dev.dart \
  --dart-define-from-file=env/dev.json

GitLab CI 片段(衔接现有 Runner,产物走 Firebase App Distribution 而非后端用的 ACR):

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 时触发,避免误发生产包