6.5 KiB
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 的
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→ 按 flavorflutter build apk/ipa --flavor {flavor} --dart-define-from-file=env/{flavor}.json→ 上传对应分发渠道。prodflavor 的构建触发条件是打 tag,不是每次 push 都触发(避免误发生产包)。
参考链接
附录: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 机制包装成统一的命令行接口。
核心概念
- Android
productFlavors:在android/app/build.gradle里声明多套applicationId/versionNameSuffix/资源目录,编译时用--flavor选择其中一套。 - iOS Scheme + xcconfig:iOS 没有 Gradle 那样的单文件配置,而是通过 Xcode 里多个 Build Configuration(对应不同的
.xcconfig文件设置 Bundle ID 等)+ 多个 Scheme 组合实现同样的效果,flutter build ipa --flavor xxx背后就是选中同名 Scheme。 - Dart 入口文件(
main_xxx.dart):flavor 决定的是"编译出什么样的原生外壳",Dart 代码本身默认只有一个main.dart入口——项目约定用多个入口文件对应各 flavor,让每个 flavor 能设置不同的启动参数(比如传给bootstrap()一个环境枚举)。 --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 时触发,避免误发生产包