构建变体与环境配置
使用各环境的资源和 Dart-define 配置,定义开发、预发布和生产变体。
构建变体与环境配置 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Build Flavors Matter
A production Flutter app rarely talks to a single backend. You need a dev build pointing at a local or staging API, a staging build for QA, and a hardened prod build for the store.
- Flavors are named build variants that can differ in app id, icon, name, and signing.
- Environment config is the data each flavor injects: base URLs, feature flags, API keys.
The goal: install dev, staging, and prod side-by-side on one device, each fully isolated and impossible to confuse.
The Two Layers: Native Flavors + Dart-Define
A clean setup separates two concerns:
- Native flavors (Android
productFlavors, iOSschemes/xcconfig) control the application identity: bundle id, display name, icon. - Dart-define values control runtime configuration your Dart code reads: API URL, environment name, log level.
Native flavors let dev, staging, and prod coexist on the device with distinct ids like com.acme.app.dev. Dart-define keeps secrets and URLs out of source control and version-pinned at compile time.
Defining a Type-Safe Environment Enum
Start in Dart with a single source of truth for which environments exist. An enum prevents typos and enables exhaustive switch handling.
This snippet is plain Dart with no Flutter dependency, so it runs anywhere.
enum Environment { dev, staging, prod }
String labelFor(Environment env) {
switch (env) {
case Environment.dev:
return 'Development';
case Environment.staging:
return 'Staging';
case Environment.prod:
return 'Production';
}
}
void main() {
for (final env in Environment.values) {
print('${env.name} -> ${labelFor(env)}');
}
}Reading Dart-Define at Compile Time
Flutter exposes compile-time constants through String.fromEnvironment, bool.fromEnvironment, and int.fromEnvironment. You pass them with --dart-define at build time.
- Values are baked into the binary; they are NOT read at runtime from the device.
- Always provide a
defaultValueso a missing define fails predictably.
Because these are const, they can be evaluated even in const contexts.
const String apiUrl = String.fromEnvironment(
'API_URL',
defaultValue: 'http://localhost:8080',
);
const String envName = String.fromEnvironment(
'ENV',
defaultValue: 'dev',
);
const bool analyticsEnabled = bool.fromEnvironment(
'ANALYTICS',
defaultValue: false,
);
void main() {
print('env=$envName url=$apiUrl analytics=$analyticsEnabled');
}Building an AppConfig Object
Scatter String.fromEnvironment calls across your codebase and you lose control. Centralize them in one immutable AppConfig that you resolve once and pass down.
This keeps every flavor difference in one auditable place and makes testing trivial: you just construct an AppConfig with the values you want.
class AppConfig {
final String envName;
final String apiUrl;
final bool analyticsEnabled;
const AppConfig({
required this.envName,
required this.apiUrl,
required this.analyticsEnabled,
});
factory AppConfig.fromEnvironment() {
return const AppConfig(
envName: String.fromEnvironment('ENV', defaultValue: 'dev'),
apiUrl: String.fromEnvironment('API_URL',
defaultValue: 'http://localhost:8080'),
analyticsEnabled:
bool.fromEnvironment('ANALYTICS', defaultValue: false),
);
}
bool get isProd => envName == 'prod';
}
void main() {
final config = AppConfig.fromEnvironment();
print('Running in ${config.envName} -> ${config.apiUrl}');
}Passing Dart-Define on the Command Line
You inject configuration when you run or build. Each --dart-define sets one key. Pair it with the native flavor via --flavor.
--flavor stagingselects the native variant (id, icon, name).--dart-definefeeds yourAppConfig.
Typing these every time is error-prone, so teams capture them in scripts or files (next scene).
flutter run \
--flavor staging \
--target lib/main.dart \
--dart-define=ENV=staging \
--dart-define=API_URL=https://staging.api.acme.com \
--dart-define=ANALYTICS=true
flutter build apk \
--release \
--flavor prod \
--dart-define=ENV=prod \
--dart-define=API_URL=https://api.acme.com \
--dart-define=ANALYTICS=trueDart-Define-From-File for Cleaner CI
Long --dart-define chains are brittle. Flutter supports --dart-define-from-file, which reads a JSON (or .env-style) file of key/value pairs.
- Keep one file per environment:
config/dev.json,config/staging.json,config/prod.json. - Commit non-secret files; inject secret ones in CI from a secure store.
Example config/prod.json and its invocation are shown. The keys map one-to-one to your fromEnvironment lookups.
// config/prod.json
{
"ENV": "prod",
"API_URL": "https://api.acme.com",
"ANALYTICS": true
}
// Invocation:
// flutter build appbundle --release \
// --flavor prod \
// --dart-define-from-file=config/prod.jsonAndroid: productFlavors
On Android, declare flavors in android/app/build.gradle. Each flavor overrides the application id suffix and name so builds install side-by-side.
applicationIdSuffixappends to the base id (e.g.com.acme.app.dev).resValueoverrides the launcher label per flavor.
A flavorDimensions entry is required before listing flavors.
android {
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationIdSuffix ".dev"
resValue "string", "app_name", "Acme Dev"
}
staging {
dimension "env"
applicationIdSuffix ".staging"
resValue "string", "app_name", "Acme Staging"
}
prod {
dimension "env"
resValue "string", "app_name", "Acme"
}
}
}iOS: Schemes and xcconfig
On iOS, flavors map to Xcode schemes backed by build configurations and .xcconfig files. Each scheme sets a distinct PRODUCT_BUNDLE_IDENTIFIER and display name.
- Create configs like
Debug-dev,Release-prod, etc. - An
.xcconfigper environment overrides the bundle id andDISPLAY_NAME, read inInfo.plistvia$(DISPLAY_NAME).
Flutter matches --flavor prod to the Xcode scheme named prod.
// ios/Flutter/staging.xcconfig
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.acme.app.staging
DISPLAY_NAME = Acme Staging
// In Info.plist:
// <key>CFBundleDisplayName</key>
// <string>$(DISPLAY_NAME)</string>Per-Environment Assets
Flavors often need different assets: a colored DEV banner, a staging app icon, a different Firebase config.
- Organize assets in folders like
assets/dev/,assets/prod/and select the path at runtime from yourAppConfig. - For native icons, Android resolves
src/dev/resautomatically; iOS uses per-config asset catalogs. - Place flavor-specific
google-services.jsonunderandroid/app/src/<flavor>/.
The Dart side simply derives the asset path from the active environment.
class AssetPaths {
final String envName;
const AssetPaths(this.envName);
String get logo => 'assets/$envName/logo.png';
String get configBanner =>
envName == 'prod' ? '' : 'assets/$envName/banner.png';
}
void main() {
for (final env in ['dev', 'staging', 'prod']) {
final paths = AssetPaths(env);
print('$env logo: ${paths.logo}');
}
}Wiring AppConfig into main()
Resolve the config once at startup and make it available to the widget tree (via an InheritedWidget, provider, or a service locator). Avoid reading fromEnvironment deep in your widgets.
- Build the config before
runApp. - Show a visible environment banner for non-prod builds to prevent QA confusion.
The snippet below is Flutter framework code, so it is not standalone-runnable, but it shows the canonical entry point.
import 'package:flutter/material.dart';
void main() {
final config = AppConfig.fromEnvironment();
runApp(MyApp(config: config));
}
class MyApp extends StatelessWidget {
final AppConfig config;
const MyApp({super.key, required this.config});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Acme (${config.envName})',
debugShowCheckedModeBanner: !config.isProd,
home: const Scaffold(body: Center(child: Text('Home'))),
);
}
}Quick Check: Where Config Lives
You configured dev, staging, and prod flavors. Test your understanding of how Dart-define values behave.
Recap
You built a complete flavor + environment configuration strategy:
- Two layers: native flavors (Android
productFlavors, iOS schemes/xcconfig) set app identity; dart-define sets runtime config. - Type safety: a single
Environmentenum and an immutableAppConfigresolved once viaAppConfig.fromEnvironment(). - Injection: pass
--flavorwith--dart-define, or scale cleanly with--dart-define-from-file=config/<env>.json. - Assets: per-environment folders and native resource overrides for icons, banners, and Firebase configs.
- Key insight: dart-define is compile-time and baked into the binary, so changing config always means a rebuild.
The payoff: dev, staging, and prod install side-by-side, fully isolated, and impossible to confuse.
常见问题解答
「构建变体与环境配置」课时是免费的吗?
是的 — 「构建变体与环境配置」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。
「构建变体与环境配置」这节课中我会学到什么?
使用各环境的资源和 Dart-define 配置,定义开发、预发布和生产变体。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Flutter Mobile Development 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「构建变体与环境配置」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Flutter Mobile Development 课中编写并运行代码吗?
能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。