多环境与 flavors 配置

开发时连测试服、发版时连生产服,如果靠“改一行地址再提交”,迟早有人把测试配置发到线上。多环境配置要解决三件事:编译期确定的环境变量、应用名与图标等外观差异、以及错误配置时尽早失败。本章对比 --dart-define 与 flavors 两种方案,并给出可直接落地的组合用法。

两种实现方式

方案配置位置优点局限
--dart-define命令行或 IDE 配置零原生改动、跨平台一致、CI 友好无法区分应用名/图标,不支持原生 SDK 差异化
flavorsAndroid productFlavors + iOS Scheme可装多个 App,图标、包名、原生配置都能分开配置繁琐,两端要分别维护
两者组合同时使用覆盖全部场景命令变长,需要脚本封装

建议:只区分接口地址与开关时用 --dart-define;需要“正式包与测试包同时装在手机上”时再上 flavors。

--dart-define 与 String.fromEnvironment

// lib/core/env.dart
class Env {
  // fromEnvironment 必须是 const 构造,默认值写在第二个参数里
  static const String name = String.fromEnvironment('ENV', defaultValue: 'dev');
  static const String apiBase = String.fromEnvironment('API_BASE', defaultValue: 'https://dev.api.example.com');
  static const bool enableLog = bool.fromEnvironment('ENABLE_LOG', defaultValue: true);

  static bool get isProd => name == 'prod';
}
flutter run --dart-define=ENV=dev --dart-define=API_BASE=https://dev.api.example.com
flutter build apk --release --dart-define=ENV=prod --dart-define=API_BASE=https://api.example.com

关键点:String.fromEnvironment 只在 const 上下文里读取编译期定义,写成 final 或运行期读取会拿到默认值——这是最常见的“配置不生效”原因。多个变量建议写进 --dart-define-from-file=config/prod.json(Flutter 3.7+ 支持),避免命令行过长。

Android product flavors

// android/app/build.gradle
android {
    flavorDimensions "env"
    productFlavors {
        dev {
            dimension "env"
            applicationIdSuffix ".dev"       // 可与正式版共存
            resValue "string", "app_name", "商城 Dev"
        }
        prod {
            dimension "env"
            resValue "string", "app_name", "商城"
        }
    }
}
flutter run --flavor dev -t lib/main_dev.dart
flutter build apk --release --flavor prod -t lib/main_prod.dart
flutter build appbundle --release --flavor prod -t lib/main_prod.dart

--flavor 必须与 Gradle 中定义的 flavor 名完全一致,且 -t 指定的入口文件必须存在;漏掉 -t 时会用 lib/main.dart,容易误连环境。

iOS Scheme 配置

  • 在 Xcode 里 Product > Scheme > Manage Schemes 复制 Runnerdevprod 两个 scheme。
  • 每个 scheme 的 Build Configuration 分别指向 Debug-dev / Release-prod 这类自定义配置,并在 Build Settings 里设置不同的 PRODUCT_BUNDLE_IDENTIFIERPRODUCT_NAME
  • flutter run --flavor dev 时 Flutter 会去找同名 scheme;scheme 名与 --flavor 不一致会直接报错。
  • iOS 不能在两端共用一个 Bundle ID 安装两份应用,测试包必须改标识符。

外观与标识差异化

项目实现方式说明
应用名Android resValue;iOS CFBundleDisplayName测试包加后缀便于辨认
图标flutter_launcher_icons 分 flavor 配置测试包用灰色或带角标图标
包名/Bundle IDapplicationIdSuffixPRODUCT_BUNDLE_IDENTIFIER决定能否与正式版共存
启动图flutter_native_splash 按 flavor 生成测试包加“DEV”水印最直观
环境标识条代码里按 Env.name 判断显示最省事也最有效,建议必备

入口文件组织

// lib/main_dev.dart
import 'package:flutter/material.dart';
import 'core/app.dart';
import 'core/env.dart';

void main() {
  assert(Env.name == 'dev'); // 防止误用正式入口
  runApp(const App(showDebugBanner: true)); // 页面顶部挂一条“测试环境”提示
}
// lib/main_prod.dart
import 'package:flutter/material.dart';
import 'core/app.dart';

void main() => runApp(const App(showDebugBanner: false));

两个入口共享 App 与全部业务代码,差异只留在配置与开关上;不要复制出两套页面代码,否则很快就不一致。

配置校验与 fail-fast

// 启动时校验,缺配置直接抛出,避免带着错地址上线
void validateEnv() {
  if (Env.apiBase.isEmpty || !Env.apiBase.startsWith('http')) {
    throw StateError('API_BASE 未配置或格式错误: "${Env.apiBase}"');
  }
  if (Env.isProd && Env.enableLog) {
    throw StateError('生产环境不应开启详细日志');
  }
}

把这段校验放在 main 的第一行。CI 里构建 prod 包时如果忘记传 --dart-define,构建会立刻失败,比线上发现接口 404 便宜得多。

与 CI 的配合

  • 每个环境一套构建命令,写进 Makefile 或脚本,禁止手工拼参数。
  • 密钥、签名口令用环境变量或 CI Secrets 注入,不要写进仓库。
  • 构建日志里打印 ENVAPI_BASE(不要打印密钥),方便排查“发错环境”。
  • 用 flavors 时 CI 需要同时构建 dev 与 prod,注意 Android 与 iOS 的 flavor 名必须一致。

常见坑

现象正确做法
final 读环境变量永远拿到默认值必须用 const + String.fromEnvironment
--flavor 名与 Gradle 不一致构建报找不到 flavor两端命名统一,脚本里集中定义
忘传 -t 入口文件测试包连了生产接口命令里显式指定 -t lib/main_xxx.dart
只改地址不改包名测试包覆盖正式包applicationIdSuffix 与 Bundle ID 区分
配置硬编码进代码换环境要改代码再发版全部走编译期注入
生产包开了日志隐私与性能风险Env.isProd 关掉,并在校验里兜底

小结:只差接口地址就用 --dart-defineString.fromEnvironment(务必 const),需要多个安装包共存再加 Android productFlavors 与 iOS Scheme;应用名、图标、包名通过 flavor 差异化,入口按 lib/main_dev.dartlib/main_prod.dart 拆分共享同一套业务代码;启动时做配置校验 fail-fast,并把命令固化进 CI,避免人工拼参数发错环境。

笔记加载中…