0Pricing
Flutter Mobile Development · 课时

构建变体与环境配置

使用各环境的资源和 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, iOS schemes/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 defaultValue so 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 staging selects the native variant (id, icon, name).
  • --dart-define feeds your AppConfig.

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=true

Dart-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.json

Android: 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.

  • applicationIdSuffix appends to the base id (e.g. com.acme.app.dev).
  • resValue overrides 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 .xcconfig per environment overrides the bundle id and DISPLAY_NAME, read in Info.plist via $(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 your AppConfig.
  • For native icons, Android resolves src/dev/res automatically; iOS uses per-config asset catalogs.
  • Place flavor-specific google-services.json under android/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 Environment enum and an immutable AppConfig resolved once via AppConfig.fromEnvironment().
  • Injection: pass --flavor with --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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 构建变体与环境配置
  2. 使用 Fastlane 和 GitHub Actions 实现自动化流水线
  3. 崩溃报告与符号化堆栈跟踪
  4. 远程配置、功能开关与分阶段发布
← 返回 Flutter Mobile Development