0Pricing
Flutter Mobile Development · 강의

빌드 플레이버 및 환경 구성

환경별 에셋과 Dart-define 구성을 사용해 개발, 스테이징 및 운영 플레이버를 정의합니다.

빌드 플레이버 및 환경 구성은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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.

자주 묻는 질문

“빌드 플레이버 및 환경 구성” 강의는 무료인가요?

네 — “빌드 플레이버 및 환경 구성” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

“빌드 플레이버 및 환경 구성”에서 뭘 배우나요?

환경별 에셋과 Dart-define 구성을 사용해 개발, 스테이징 및 운영 플레이버를 정의합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“빌드 플레이버 및 환경 구성” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 빌드 플레이버 및 환경 구성
  2. Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인
  3. 충돌 보고 및 기호화된 스택 추적
  4. 원격 구성, 기능 플래그 및 단계적 출시
← Flutter Mobile Development(으)로 돌아가기