Flutter 使用 flutter_flavorizr 多渠道打包

前言:

在 Flutter 项目里,随着业务逐渐复杂,通常都会遇到多环境、多渠道打包的问题。比如开发环境、测试环境、预发布环境、生产环境需要使用不同的接口地址、App 名称、包名、图标、签名配置,甚至三方 SDK 配置也可能不同。
如果每次打包都手动改 applicationIdBundle Identifier、App 名称、接口地址,不仅麻烦,而且很容易出错。比如测试包误连生产接口、生产包用了测试图标、iOS Scheme 配错、Android 包名冲突等。
这篇文章结合 Flutter 项目实践,聊一聊如何使用 flutter_flavorizr 做多渠道/多环境打包。文章不会只讲一个命令,而是从真实项目角度讲清楚:为什么需要 flavor、Flutter 多环境如何设计、flutter_flavorizr 怎么配置、Android/iOS 会生成什么、Dart 层如何区分环境、常用打包命令怎么写,以及实际落地时容易遇到哪些问题。

正文:

这篇文章主要从下面几个方面展开:

  1. 为什么需要多渠道打包
  2. Flutter flavor 是什么
  3. flutter_flavorizr 能解决什么问题
  4. 项目环境如何划分
  5. 安装 flutter_flavorizr
  6. 如何配置 pubspec.yaml
  7. Android 会生成哪些配置
  8. iOS 会生成哪些配置
  9. Dart 层如何读取当前环境
  10. 不同环境如何切换接口地址
  11. 常用运行和打包命令
  12. CI/CD 中如何使用
  13. 常见问题和解决方式
  14. 实际项目中的推荐规范
为什么需要多渠道打包

一个 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 原生里通常通过 SchemeConfigurationBundle 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,再执行生成命令。

官方地址:

项目环境如何划分

以一个常见项目为例,可以先规划三个环境:

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.gradlebuild.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 层业务配置。

推荐规范

实际项目里,我建议遵守下面这些规范:

  1. flavor 名称统一使用 devtestprod
  2. Android applicationId 和 iOS bundleId 必须按环境区分
  3. App 名称必须能区分测试包和生产包
  4. 不同环境最好使用不同图标
  5. Dart 层环境通过 --dart-define 注入
  6. 接口地址统一从 EnvConfig 读取
  7. 日志开关统一从环境配置读取
  8. 三方 SDK 配置按环境隔离
  9. 打包命令写成脚本,不靠手敲
  10. CI 中明确指定 flavor 和 dart-define
  11. 生产包打包前打印并确认当前环境
  12. 不要把生产敏感配置明文提交到仓库
  13. iOS Scheme 要设置 Shared
  14. 每次改 flavor 配置后都要重新验证 Android 和 iOS
  15. 文档里写清楚每个环境的用途和打包命令

结束:

这篇文章就先写到这里。

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 负责固定打包命令

这样项目不管是本地开发、测试分发,还是生产发布,都能有一套稳定、清晰、可维护的多环境打包方案。

参考资料:

相关推荐
环境工程笔记1 小时前
浏览器自动化跑成功了,为什么结果还是不对?
前端
东风破_1 小时前
一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?
前端·javascript
问心无愧05131 小时前
ctf show web入门261
android·前端·笔记
触底反弹1 小时前
你真的理解 JavaScript 变量提升(Hoisting)吗?从 V8 引擎编译原理深入剖析
前端·面试
蜡台2 小时前
Vue2 使用 typescript 教程
前端·vue.js·typescript
光影少年2 小时前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
云水一下2 小时前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript
G_dou_2 小时前
# Flutter+OpenHarmony 实战:note_app 笔记应用
flutter·harmonyos