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

127 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** 是 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 + 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<void> 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 时触发,避免误发生产包
```