Flutter 使用 flutter_flavorizr 多渠道打包

3 阅读12分钟

前言:

  • 在 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 项目最开始可能只有一个环境:

生产环境

所有人都跑同一个 App,接口地址也是固定的。

但真实项目里通常会逐渐变成这样:

开发环境 dev
测试环境 test
预发布环境 staging
生产环境 prod

不同环境可能有不同配置:

App 名称
包名 / Bundle ID
接口 baseUrl
App 图标
启动页
签名配置
推送配置
统计配置
支付配置
分享配置
日志开关
Debug 面板开关

例如:

开发包:MyApp Dev
测试包:MyApp Test
正式包:MyApp

Android 包名可能是:

com.example.app.dev
com.example.app.test
com.example.app

iOS Bundle ID 可能是:

com.example.app.dev
com.example.app.test
com.example.app

如果这些都靠手动改,很容易出现问题。

比较典型的事故有:

  • 测试包误用了生产接口
  • 生产包打开了 debug 日志
  • iOS 打包时选错 Scheme
  • Android 包名和线上包冲突
  • 测试包覆盖了正式包
  • 三方平台配置和包名不匹配
  • 图标和 App 名称没有区分环境

所以多渠道打包不是锦上添花,而是项目工程化里非常基础的一环。

  • Flutter flavor 是什么

在 Flutter 里,多环境通常会用到 flavor。

可以简单理解为:

flavor = 一个 App 的不同构建变体

比如:

dev
test
prod

每个 flavor 可以有自己的:

  • App 名称
  • 包名
  • Bundle ID
  • 图标
  • 启动入口
  • 原生配置
  • Dart 编译参数

Android 原生里有 productFlavors

iOS 原生里通常通过 SchemeConfigurationBundle Identifier 来区分。

Flutter 通过命令指定 flavor:

flutter run --flavor dev
flutter build apk --flavor prod
flutter build ipa --flavor prod

但问题是,Android 和 iOS 的 flavor 配置比较繁琐。

如果手动配,需要同时改:

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,再执行生成命令。

官方地址:

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

dev:开发环境
test:测试环境
prod:生产环境

对应关系可以这样设计:

环境App 名称Android 包名iOS Bundle ID接口
devDemo Devcom.example.demo.devcom.example.demo.devdev-api
testDemo Testcom.example.demo.testcom.example.demo.testtest-api
prodDemocom.example.democom.example.demoprod-api

这样做的好处是:

  • 三个 App 可以同时安装在手机上

  • 图标和名称能明显区分环境

  • 包名不会冲突

  • 三方 SDK 可以按环境分别配置

  • 打包命令更明确

  • 安装 flutter_flavorizr

通常把 flutter_flavorizr 放到 dev_dependencies 中:

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_flavorizr: ^2.4.2

然后执行:

flutter pub get

版本可以以 pub.dev 当前最新版本为准。写文章时我看到 pub.dev 上 flutter_flavorizr 的最新版本是 2.4.2

  • 配置 pubspec.yaml

pubspec.yaml 中增加 flavorizr 配置。

示例:

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"

配置完成后执行:

dart run flutter_flavorizr

或者:

flutter pub run flutter_flavorizr

执行后,工具会根据配置修改 Android 和 iOS 工程。

  • 配置不同环境图标

如果不同环境要使用不同图标,可以给每个 flavor 配图标。

例如:

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"

建议图标目录按环境放:

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。

概念上类似:

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:

flutter run --flavor dev
flutter build apk --flavor test
flutter build appbundle --flavor prod
  • iOS 侧生成了什么

iOS 侧通常会生成不同 Scheme 和 Configuration。

例如:

Runner-dev
Runner-test
Runner-prod

每个 Scheme 对应不同的 Bundle ID 和 App 名称。

打包时可以指定:

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 检查一遍:

Runner -> Targets -> Signing & Capabilities

确认每个 flavor 的 Bundle ID 和签名配置是否正确。

  • Dart 层如何识别当前环境

原生 flavor 配好后,Dart 层还需要知道当前运行的是哪个环境。

常见做法有三种。

第一种:不同入口文件。

lib/main_dev.dart
lib/main_test.dart
lib/main_prod.dart

例如:

import 'app.dart';
import 'env.dart';

void main() {
  Env.init(Environment.dev);
  runApp(const MyApp());
}

main_prod.dart

import 'app.dart';
import 'env.dart';

void main() {
  Env.init(Environment.prod);
  runApp(const MyApp());
}

运行时指定入口:

flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor prod -t lib/main_prod.dart

第二种:使用 --dart-define

flutter run --flavor dev --dart-define=APP_ENV=dev
flutter run --flavor prod --dart-define=APP_ENV=prod

Dart 里读取:

const appEnv = String.fromEnvironment('APP_ENV', defaultValue: 'dev');

第三种:不同 flavor 生成不同配置文件。

比如:

assets/config/dev.json
assets/config/test.json
assets/config/prod.json

然后根据环境加载对应配置。

我个人更推荐:

flavor 负责原生包信息
dart-define 负责 Dart 层环境变量

这样比较清晰。

  • 环境配置类设计

可以定义一个环境枚举:

enum AppEnv {
  dev,
  test,
  prod,
}

再定义环境配置:

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,
  });
}

配置管理:

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,可以这样初始化:

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;
  }
}

网络层使用:

final dio = Dio(
  BaseOptions(
    baseUrl: Env.config.baseUrl,
  ),
);

日志开关:

if (Env.config.enableLog) {
  dio.interceptors.add(LogInterceptor(responseBody: true));
}
  • 常用运行命令

开发环境运行:

flutter run --flavor dev --dart-define=APP_ENV=dev

测试环境运行:

flutter run --flavor test --dart-define=APP_ENV=test

生产环境运行:

flutter run --flavor prod --dart-define=APP_ENV=prod

如果使用不同入口文件:

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:

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:

flutter build appbundle --flavor prod --dart-define=APP_ENV=prod

iOS:

flutter build ipa --flavor prod --dart-define=APP_ENV=prod
  • 建议写成脚本

命令长了之后,不建议每次手敲。

可以写一个 tool/build.sh

#!/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"

运行:

./tool/build.sh dev
./tool/build.sh prod

也可以拆成:

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 包:

- name: Pub get
  run: flutter pub get

- name: Build prod apk
  run: flutter build apk --flavor prod --dart-define=APP_ENV=prod

测试包:

- name: Build test apk
  run: flutter build apk --flavor test --dart-define=APP_ENV=test

如果 iOS 打包,需要额外处理证书、描述文件和 Xcode signing。

CI 中建议把下面这些变量配置成安全变量:

APP_ENV
API_BASE_URL
SENTRY_DSN
UMENG_KEY
JPUSH_KEY
FIREBASE_CONFIG

不要把生产环境敏感配置直接写死在仓库里。

  • 三方 SDK 配置如何区分环境

多渠道打包里,三方 SDK 是一个重点。

例如:

  • Firebase
  • 友盟
  • 极光推送
  • 微信登录
  • 支付宝支付
  • 高德地图
  • Sentry
  • Bugly

这些 SDK 可能会根据包名、Bundle ID、配置文件区分环境。

Android 可能需要不同的:

google-services.json
agconnect-services.json
AndroidManifest meta-data

iOS 可能需要不同的:

GoogleService-Info.plist
Info.plist 配置
URL Schemes
Associated Domains
Entitlements

建议按环境放配置文件:

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

执行:

flutter run --flavor dev

如果报 Scheme 找不到,通常是 iOS Scheme 没生成成功,或者 Xcode 没同步。

可以检查:

ios/Runner.xcodeproj/xcshareddata/xcschemes

也可以打开 Xcode:

Product -> Scheme -> Manage Schemes

确认对应 Scheme 是否存在,并且是否勾选 Shared。

  • 常见问题2:Android applicationId 没变

如果 Android 安装后还是覆盖正式包,说明 flavor 的 applicationId 没生效。

检查:

android/app/build.gradle

确认是否有:

productFlavors

以及每个 flavor 是否配置了不同的 applicationId

  • 常见问题3:App 名称没有变化

Android 需要确认 resValue 或资源文件是否生成正确。

iOS 需要确认 Info.plist 或 build settings 中 App 名称是否按 Scheme 区分。

有时候需要执行:

flutter clean
flutter pub get

然后重新运行。

  • 常见问题4:Dart 层环境没有切换

原生 flavor 成功,不代表 Dart 层环境自动切换。

比如执行了:

flutter run --flavor prod

但 Dart 里如果没有读取 flavor 或 dart-define,接口地址仍然可能是 dev。

所以建议命令里始终带上:

--dart-define=APP_ENV=prod

并在 App 启动时打印当前环境:

debugPrint('Current env: ${Env.config.env}');
debugPrint('Current baseUrl: ${Env.config.baseUrl}');
  • 常见问题5:生产包打开了日志

这通常是环境配置没有统一收口。

建议所有日志开关都从环境配置读取:

if (Env.config.enableLog) {
  // add log interceptor
}

生产环境:

enableLog: false

不要在业务页面里到处写:

if (kDebugMode) {}

因为 kDebugMode 只能区分 debug/release,不能区分 dev/test/prod。

  • 常见问题6:多环境接口地址写散了

不要在项目里到处写:

'https://dev-api.example.com'

应该统一从环境配置读取:

Env.config.baseUrl

否则后面换域名、加预发环境、做私有化部署都会非常麻烦。

  • 常见问题7:测试包和正式包不能共存

如果两个包不能同时安装,说明 Android applicationId 或 iOS bundleId 没有区分。

建议:

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

常用处理:

flutter clean
cd ios
pod install
cd ..
flutter pub get
  • 推荐项目结构

一个比较清晰的多环境结构可以这样设计:

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

如果使用多个入口文件:

lib/
  main_dev.dart
  main_test.dart
  main_prod.dart

如果使用 dart-define,一个入口文件也可以:

lib/main.dart

我更推荐:

原生 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 流程一起规范起来。

我比较推荐的实践是:

flutter_flavorizr 负责生成原生 flavor
dart-define 负责注入 Dart 环境
EnvConfig 负责统一管理业务配置
脚本和 CI 负责固定打包命令

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

参考资料: