前言:
在 Flutter 项目里,随着业务逐渐复杂,通常都会遇到多环境、多渠道打包的问题。比如开发环境、测试环境、预发布环境、生产环境需要使用不同的接口地址、App 名称、包名、图标、签名配置,甚至三方 SDK 配置也可能不同。
如果每次打包都手动改 applicationId、Bundle Identifier、App 名称、接口地址,不仅麻烦,而且很容易出错。比如测试包误连生产接口、生产包用了测试图标、iOS Scheme 配错、Android 包名冲突等。
这篇文章结合 Flutter 项目实践,聊一聊如何使用 flutter_flavorizr 做多渠道/多环境打包。文章不会只讲一个命令,而是从真实项目角度讲清楚:为什么需要 flavor、Flutter 多环境如何设计、flutter_flavorizr 怎么配置、Android/iOS 会生成什么、Dart 层如何区分环境、常用打包命令怎么写,以及实际落地时容易遇到哪些问题。
正文:
这篇文章主要从下面几个方面展开:
- 为什么需要多渠道打包
- Flutter flavor 是什么
flutter_flavorizr能解决什么问题- 项目环境如何划分
- 安装
flutter_flavorizr - 如何配置
pubspec.yaml - Android 会生成哪些配置
- iOS 会生成哪些配置
- Dart 层如何读取当前环境
- 不同环境如何切换接口地址
- 常用运行和打包命令
- CI/CD 中如何使用
- 常见问题和解决方式
- 实际项目中的推荐规范
为什么需要多渠道打包
一个 Flutter 项目最开始可能只有一个环境:
text
生产环境
所有人都跑同一个 App,接口地址也是固定的。
但真实项目里通常会逐渐变成这样:
text
开发环境 dev
测试环境 test
预发布环境 staging
生产环境 prod
不同环境可能有不同配置:
text
App 名称
包名 / Bundle ID
接口 baseUrl
App 图标
启动页
签名配置
推送配置
统计配置
支付配置
分享配置
日志开关
Debug 面板开关
例如:
text
开发包:MyApp Dev
测试包:MyApp Test
正式包:MyApp
Android 包名可能是:
text
com.example.app.dev
com.example.app.test
com.example.app
iOS Bundle ID 可能是:
text
com.example.app.dev
com.example.app.test
com.example.app
如果这些都靠手动改,很容易出现问题。
比较典型的事故有:
- 测试包误用了生产接口
- 生产包打开了 debug 日志
- iOS 打包时选错 Scheme
- Android 包名和线上包冲突
- 测试包覆盖了正式包
- 三方平台配置和包名不匹配
- 图标和 App 名称没有区分环境
所以多渠道打包不是锦上添花,而是项目工程化里非常基础的一环。
Flutter flavor 是什么
在 Flutter 里,多环境通常会用到 flavor。
可以简单理解为:
text
flavor = 一个 App 的不同构建变体
比如:
text
dev
test
prod
每个 flavor 可以有自己的:
- App 名称
- 包名
- Bundle ID
- 图标
- 启动入口
- 原生配置
- Dart 编译参数
Android 原生里有 productFlavors。
iOS 原生里通常通过 Scheme、Configuration、Bundle Identifier 来区分。
Flutter 通过命令指定 flavor:
bash
flutter run --flavor dev
flutter build apk --flavor prod
flutter build ipa --flavor prod
但问题是,Android 和 iOS 的 flavor 配置比较繁琐。
如果手动配,需要同时改:
text
android/app/build.gradle
android/app/src/dev
android/app/src/test
ios/Runner.xcodeproj
ios/Runner.xcworkspace
ios Schemes
ios Configurations
这就是 flutter_flavorizr 的价值。
flutter_flavorizr 是什么
flutter_flavorizr 是一个 Flutter 多 flavor 配置生成工具。
它可以根据 pubspec.yaml 里的配置,自动生成 Android 和 iOS 的 flavor 配置。
根据官方文档,它支持通过配置生成不同 flavor 的:
- Android applicationId
- Android App 名称
- iOS Bundle ID
- iOS App 名称
- iOS Scheme
- 图标资源
- flavorizr 自动化指令
也就是说,我们不需要手动在 Android Studio 和 Xcode 里一点点配置 flavor,而是把配置写到 pubspec.yaml,再执行生成命令。
官方地址:
-
pub.dev:flutter_flavorizr
-
GitHub:flutter_flavorizr repository
项目环境如何划分
以一个常见项目为例,可以先规划三个环境:
text
dev:开发环境
test:测试环境
prod:生产环境
对应关系可以这样设计:
| 环境 | App 名称 | Android 包名 | iOS Bundle ID | 接口 |
|---|---|---|---|---|
| dev | Demo Dev | com.example.demo.dev | com.example.demo.dev | dev-api |
| test | Demo Test | com.example.demo.test | com.example.demo.test | test-api |
| prod | Demo | com.example.demo | com.example.demo | prod-api |
这样做的好处是:
-
三个 App 可以同时安装在手机上
-
图标和名称能明显区分环境
-
包名不会冲突
-
三方 SDK 可以按环境分别配置
-
打包命令更明确
安装 flutter_flavorizr
通常把 flutter_flavorizr 放到 dev_dependencies 中:
yaml
dev_dependencies:
flutter_test:
sdk: flutter
flutter_flavorizr: ^2.4.2
然后执行:
bash
flutter pub get
版本可以以 pub.dev 当前最新版本为准。写文章时我看到 pub.dev 上
flutter_flavorizr的最新版本是2.4.2。
配置 pubspec.yaml
在 pubspec.yaml 中增加 flavorizr 配置。
示例:
yaml
flavorizr:
app:
android:
flavorDimensions: "environment"
ios:
flavors:
dev:
app:
name: "Demo Dev"
android:
applicationId: "com.example.demo.dev"
ios:
bundleId: "com.example.demo.dev"
test:
app:
name: "Demo Test"
android:
applicationId: "com.example.demo.test"
ios:
bundleId: "com.example.demo.test"
prod:
app:
name: "Demo"
android:
applicationId: "com.example.demo"
ios:
bundleId: "com.example.demo"
配置完成后执行:
bash
dart run flutter_flavorizr
或者:
bash
flutter pub run flutter_flavorizr
执行后,工具会根据配置修改 Android 和 iOS 工程。
配置不同环境图标
如果不同环境要使用不同图标,可以给每个 flavor 配图标。
例如:
yaml
flavorizr:
flavors:
dev:
app:
name: "Demo Dev"
icon: "assets/flavors/dev/app_icon.png"
android:
applicationId: "com.example.demo.dev"
ios:
bundleId: "com.example.demo.dev"
test:
app:
name: "Demo Test"
icon: "assets/flavors/test/app_icon.png"
android:
applicationId: "com.example.demo.test"
ios:
bundleId: "com.example.demo.test"
prod:
app:
name: "Demo"
icon: "assets/flavors/prod/app_icon.png"
android:
applicationId: "com.example.demo"
ios:
bundleId: "com.example.demo"
建议图标目录按环境放:
text
assets/
flavors/
dev/
app_icon.png
test/
app_icon.png
prod/
app_icon.png
这样环境差异比较清晰。
Android 侧生成了什么
执行 flutter_flavorizr 后,Android 侧通常会生成或修改 flavor 配置。
核心是 android/app/build.gradle 或 build.gradle.kts 中的 product flavors。
概念上类似:
gradle
flavorDimensions "environment"
productFlavors {
dev {
dimension "environment"
applicationId "com.example.demo.dev"
resValue "string", "app_name", "Demo Dev"
}
test {
dimension "environment"
applicationId "com.example.demo.test"
resValue "string", "app_name", "Demo Test"
}
prod {
dimension "environment"
applicationId "com.example.demo"
resValue "string", "app_name", "Demo"
}
}
这样 Android 打包时就可以指定 flavor:
bash
flutter run --flavor dev
flutter build apk --flavor test
flutter build appbundle --flavor prod
iOS 侧生成了什么
iOS 侧通常会生成不同 Scheme 和 Configuration。
例如:
text
Runner-dev
Runner-test
Runner-prod
每个 Scheme 对应不同的 Bundle ID 和 App 名称。
打包时可以指定:
bash
flutter run --flavor dev
flutter build ipa --flavor prod
iOS 这里最容易出问题。
因为 iOS 除了 Bundle ID,还会涉及:
- Scheme
- Build Configuration
- Provisioning Profile
- Signing Certificate
- Apple Developer 后台 App ID
- 推送、Associated Domains 等能力
所以 iOS flavor 生成后,建议打开 Xcode 检查一遍:
text
Runner -> Targets -> Signing & Capabilities
确认每个 flavor 的 Bundle ID 和签名配置是否正确。
Dart 层如何识别当前环境
原生 flavor 配好后,Dart 层还需要知道当前运行的是哪个环境。
常见做法有三种。
第一种:不同入口文件。
text
lib/main_dev.dart
lib/main_test.dart
lib/main_prod.dart
例如:
dart
import 'app.dart';
import 'env.dart';
void main() {
Env.init(Environment.dev);
runApp(const MyApp());
}
main_prod.dart:
dart
import 'app.dart';
import 'env.dart';
void main() {
Env.init(Environment.prod);
runApp(const MyApp());
}
运行时指定入口:
bash
flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor prod -t lib/main_prod.dart
第二种:使用 --dart-define。
bash
flutter run --flavor dev --dart-define=APP_ENV=dev
flutter run --flavor prod --dart-define=APP_ENV=prod
Dart 里读取:
dart
const appEnv = String.fromEnvironment('APP_ENV', defaultValue: 'dev');
第三种:不同 flavor 生成不同配置文件。
比如:
text
assets/config/dev.json
assets/config/test.json
assets/config/prod.json
然后根据环境加载对应配置。
我个人更推荐:
text
flavor 负责原生包信息
dart-define 负责 Dart 层环境变量
这样比较清晰。
环境配置类设计
可以定义一个环境枚举:
dart
enum AppEnv {
dev,
test,
prod,
}
再定义环境配置:
dart
class EnvConfig {
final AppEnv env;
final String appName;
final String baseUrl;
final bool enableLog;
const EnvConfig({
required this.env,
required this.appName,
required this.baseUrl,
required this.enableLog,
});
}
配置管理:
dart
class Env {
static late final EnvConfig config;
static void init(AppEnv env) {
switch (env) {
case AppEnv.dev:
config = const EnvConfig(
env: AppEnv.dev,
appName: 'Demo Dev',
baseUrl: 'https://dev-api.example.com',
enableLog: true,
);
break;
case AppEnv.test:
config = const EnvConfig(
env: AppEnv.test,
appName: 'Demo Test',
baseUrl: 'https://test-api.example.com',
enableLog: true,
);
break;
case AppEnv.prod:
config = const EnvConfig(
env: AppEnv.prod,
appName: 'Demo',
baseUrl: 'https://api.example.com',
enableLog: false,
);
break;
}
}
}
如果使用 --dart-define,可以这样初始化:
dart
const envName = String.fromEnvironment('APP_ENV', defaultValue: 'dev');
void main() {
Env.init(_parseEnv(envName));
runApp(const MyApp());
}
AppEnv _parseEnv(String value) {
switch (value) {
case 'prod':
return AppEnv.prod;
case 'test':
return AppEnv.test;
case 'dev':
default:
return AppEnv.dev;
}
}
网络层使用:
dart
final dio = Dio(
BaseOptions(
baseUrl: Env.config.baseUrl,
),
);
日志开关:
dart
if (Env.config.enableLog) {
dio.interceptors.add(LogInterceptor(responseBody: true));
}
常用运行命令
开发环境运行:
bash
flutter run --flavor dev --dart-define=APP_ENV=dev
测试环境运行:
bash
flutter run --flavor test --dart-define=APP_ENV=test
生产环境运行:
bash
flutter run --flavor prod --dart-define=APP_ENV=prod
如果使用不同入口文件:
bash
flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor test -t lib/main_test.dart
flutter run --flavor prod -t lib/main_prod.dart
Android APK:
bash
flutter build apk --flavor dev --dart-define=APP_ENV=dev
flutter build apk --flavor test --dart-define=APP_ENV=test
flutter build apk --flavor prod --dart-define=APP_ENV=prod
Android AppBundle:
bash
flutter build appbundle --flavor prod --dart-define=APP_ENV=prod
iOS:
bash
flutter build ipa --flavor prod --dart-define=APP_ENV=prod
建议写成脚本
命令长了之后,不建议每次手敲。
可以写一个 tool/build.sh:
bash
#!/bin/bash
ENV=$1
if [ -z "$ENV" ]; then
echo "Usage: ./tool/build.sh dev|test|prod"
exit 1
fi
flutter clean
flutter pub get
flutter build apk \
--flavor "$ENV" \
--dart-define=APP_ENV="$ENV"
运行:
bash
./tool/build.sh dev
./tool/build.sh prod
也可以拆成:
text
tool/run_dev.sh
tool/run_test.sh
tool/build_prod_apk.sh
tool/build_prod_ipa.sh
团队项目里,把命令固定下来非常重要。
否则每个人打包命令不一样,很容易出问题。
CI/CD 中如何使用
在 CI 里,多渠道打包也应该明确指定 flavor 和 dart-define。
例如 Android prod 包:
yaml
- name: Pub get
run: flutter pub get
- name: Build prod apk
run: flutter build apk --flavor prod --dart-define=APP_ENV=prod
测试包:
yaml
- name: Build test apk
run: flutter build apk --flavor test --dart-define=APP_ENV=test
如果 iOS 打包,需要额外处理证书、描述文件和 Xcode signing。
CI 中建议把下面这些变量配置成安全变量:
text
APP_ENV
API_BASE_URL
SENTRY_DSN
UMENG_KEY
JPUSH_KEY
FIREBASE_CONFIG
不要把生产环境敏感配置直接写死在仓库里。
三方 SDK 配置如何区分环境
多渠道打包里,三方 SDK 是一个重点。
例如:
- Firebase
- 友盟
- 极光推送
- 微信登录
- 支付宝支付
- 高德地图
- Sentry
- Bugly
这些 SDK 可能会根据包名、Bundle ID、配置文件区分环境。
Android 可能需要不同的:
text
google-services.json
agconnect-services.json
AndroidManifest meta-data
iOS 可能需要不同的:
text
GoogleService-Info.plist
Info.plist 配置
URL Schemes
Associated Domains
Entitlements
建议按环境放配置文件:
text
config/
dev/
google-services.json
GoogleService-Info.plist
test/
google-services.json
GoogleService-Info.plist
prod/
google-services.json
GoogleService-Info.plist
然后在打包脚本或 flavor 配置中复制到对应位置。
不要让测试包和生产包共用同一套三方配置。
常见问题1:iOS 找不到 Scheme
执行:
bash
flutter run --flavor dev
如果报 Scheme 找不到,通常是 iOS Scheme 没生成成功,或者 Xcode 没同步。
可以检查:
text
ios/Runner.xcodeproj/xcshareddata/xcschemes
也可以打开 Xcode:
text
Product -> Scheme -> Manage Schemes
确认对应 Scheme 是否存在,并且是否勾选 Shared。
常见问题2:Android applicationId 没变
如果 Android 安装后还是覆盖正式包,说明 flavor 的 applicationId 没生效。
检查:
text
android/app/build.gradle
确认是否有:
gradle
productFlavors
以及每个 flavor 是否配置了不同的 applicationId。
常见问题3:App 名称没有变化
Android 需要确认 resValue 或资源文件是否生成正确。
iOS 需要确认 Info.plist 或 build settings 中 App 名称是否按 Scheme 区分。
有时候需要执行:
bash
flutter clean
flutter pub get
然后重新运行。
常见问题4:Dart 层环境没有切换
原生 flavor 成功,不代表 Dart 层环境自动切换。
比如执行了:
bash
flutter run --flavor prod
但 Dart 里如果没有读取 flavor 或 dart-define,接口地址仍然可能是 dev。
所以建议命令里始终带上:
bash
--dart-define=APP_ENV=prod
并在 App 启动时打印当前环境:
dart
debugPrint('Current env: ${Env.config.env}');
debugPrint('Current baseUrl: ${Env.config.baseUrl}');
常见问题5:生产包打开了日志
这通常是环境配置没有统一收口。
建议所有日志开关都从环境配置读取:
dart
if (Env.config.enableLog) {
// add log interceptor
}
生产环境:
dart
enableLog: false
不要在业务页面里到处写:
dart
if (kDebugMode) {}
因为 kDebugMode 只能区分 debug/release,不能区分 dev/test/prod。
常见问题6:多环境接口地址写散了
不要在项目里到处写:
dart
'https://dev-api.example.com'
应该统一从环境配置读取:
dart
Env.config.baseUrl
否则后面换域名、加预发环境、做私有化部署都会非常麻烦。
常见问题7:测试包和正式包不能共存
如果两个包不能同时安装,说明 Android applicationId 或 iOS bundleId 没有区分。
建议:
text
dev -> com.example.demo.dev
test -> com.example.demo.test
prod -> com.example.demo
iOS 同理。
常见问题8:生成 flavor 后 Xcode 配置冲突
iOS flavor 比 Android 更容易出现配置冲突。
如果 Xcode 报错,可以重点检查:
- Scheme 是否存在
- Scheme 是否 Shared
- Bundle ID 是否正确
- Signing Team 是否正确
- Provisioning Profile 是否匹配
- Build Configuration 是否完整
- Pod 是否需要重新 install
常用处理:
bash
flutter clean
cd ios
pod install
cd ..
flutter pub get
推荐项目结构
一个比较清晰的多环境结构可以这样设计:
text
lib/
main.dart
app.dart
env/
app_env.dart
env_config.dart
assets/
flavors/
dev/
app_icon.png
test/
app_icon.png
prod/
app_icon.png
tool/
run_dev.sh
run_test.sh
build_prod_apk.sh
build_prod_ipa.sh
如果使用多个入口文件:
text
lib/
main_dev.dart
main_test.dart
main_prod.dart
如果使用 dart-define,一个入口文件也可以:
text
lib/main.dart
我更推荐:
text
原生 flavor + dart-define + 统一 EnvConfig
这样既能控制原生包信息,也能控制 Dart 层业务配置。
推荐规范
实际项目里,我建议遵守下面这些规范:
- flavor 名称统一使用
dev、test、prod - Android applicationId 和 iOS bundleId 必须按环境区分
- App 名称必须能区分测试包和生产包
- 不同环境最好使用不同图标
- Dart 层环境通过
--dart-define注入 - 接口地址统一从
EnvConfig读取 - 日志开关统一从环境配置读取
- 三方 SDK 配置按环境隔离
- 打包命令写成脚本,不靠手敲
- CI 中明确指定 flavor 和 dart-define
- 生产包打包前打印并确认当前环境
- 不要把生产敏感配置明文提交到仓库
- iOS Scheme 要设置 Shared
- 每次改 flavor 配置后都要重新验证 Android 和 iOS
- 文档里写清楚每个环境的用途和打包命令
结束:
这篇文章就先写到这里。
Flutter 多渠道打包并不是只为"换个 App 名称"服务,它真正解决的是项目环境隔离和工程交付稳定性问题。
一个项目只要进入真实开发流程,通常都会需要:
- 开发环境
- 测试环境
- 预发布环境
- 生产环境
如果这些环境没有清晰隔离,后面一定会遇到各种问题:
- 测试包连生产接口
- 正式包打开调试日志
- 三方 SDK 配错
- 包名冲突
- iOS Scheme 混乱
- CI 打包不可控
flutter_flavorizr 的价值在于,它把 Android 和 iOS 复杂的 flavor 配置收敛到 pubspec.yaml 里,通过命令自动生成平台配置。
但工具只是第一步。
真正落地时,还要把 Dart 层环境、接口地址、日志开关、三方 SDK、打包脚本、CI 流程一起规范起来。
我比较推荐的实践是:
text
flutter_flavorizr 负责生成原生 flavor
dart-define 负责注入 Dart 环境
EnvConfig 负责统一管理业务配置
脚本和 CI 负责固定打包命令
这样项目不管是本地开发、测试分发,还是生产发布,都能有一套稳定、清晰、可维护的多环境打包方案。