Flavors & Environment #

Aplikasi produksi selalu membutuhkan lebih dari satu environment: development untuk eksperimen, staging untuk QA, production untuk pengguna nyata. Setiap environment punya URL API yang berbeda, API key berbeda, dan mungkin nama app serta ikon yang berbeda agar mudah dibedakan. Flutter mendukung ini via flavors (konfigurasi per environment di level native) dan --dart-define (variabel yang di-inject saat build).

Pendekatan: dart-define (Paling Sederhana) #

Untuk proyek kecil hingga menengah, --dart-define sudah cukup tanpa perlu setup flavor native yang kompleks:

# Jalankan dengan variabel environment
flutter run \
  --dart-define=APP_ENV=development \
  --dart-define=API_URL=https://api-dev.contoh.com \
  --dart-define=API_KEY=dev-key-abc123 \
  --dart-define=ENABLE_LOGS=true

# Build release staging
flutter build apk --release \
  --dart-define=APP_ENV=staging \
  --dart-define=API_URL=https://api-staging.contoh.com \
  --dart-define=API_KEY=staging-key-xyz789 \
  --dart-define=ENABLE_LOGS=true

# Build release production
flutter build appbundle --release \
  --dart-define=APP_ENV=production \
  --dart-define=API_URL=https://api.contoh.com \
  --dart-define=API_KEY=prod-key-secret \
  --dart-define=ENABLE_LOGS=false

AppConfig — Akses Variabel di Dart #

// lib/core/config/app_config.dart
enum AppEnvironment { development, staging, production }

class AppConfig {
  // Baca dari --dart-define, fallback ke nilai default
  static const String _env = String.fromEnvironment(
    'APP_ENV',
    defaultValue: 'development',
  );

  static const String apiUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'https://api-dev.contoh.com',
  );

  static const String apiKey = String.fromEnvironment(
    'API_KEY',
    defaultValue: '',
  );

  static const bool enableLogs = bool.fromEnvironment(
    'ENABLE_LOGS',
    defaultValue: true,
  );

  // Environment saat ini
  static AppEnvironment get environment => switch (_env) {
    'staging' => AppEnvironment.staging,
    'production' => AppEnvironment.production,
    _ => AppEnvironment.development,
  };

  static bool get isDevelopment => environment == AppEnvironment.development;
  static bool get isStaging => environment == AppEnvironment.staging;
  static bool get isProduction => environment == AppEnvironment.production;

  // Nilai yang berbeda per environment
  static int get cacheTimeout => switch (environment) {
    AppEnvironment.development => 0,       // no cache saat dev
    AppEnvironment.staging => 5,           // 5 menit
    AppEnvironment.production => 30,       // 30 menit
  };

  static String get appName => switch (environment) {
    AppEnvironment.development => '[DEV] Toko Saya',
    AppEnvironment.staging => '[STG] Toko Saya',
    AppEnvironment.production => 'Toko Saya',
  };
}

File Konfigurasi per Environment #

# Simpan konfigurasi di file dart-define
# Jangan commit ke git! Tambahkan ke .gitignore

# .dart_define/development.json
{
  "APP_ENV": "development",
  "API_URL": "https://api-dev.contoh.com",
  "API_KEY": "dev-key-abc123",
  "ENABLE_LOGS": "true"
}

# .dart_define/production.json
{
  "APP_ENV": "production",
  "API_URL": "https://api.contoh.com",
  "API_KEY": "prod-key-secret",
  "ENABLE_LOGS": "false"
}
# Jalankan dengan file JSON
flutter run --dart-define-from-file=.dart_define/development.json
flutter build appbundle --dart-define-from-file=.dart_define/production.json

Flutter Flavorizr — Setup Flavor Native #

Untuk proyek yang butuh nama app, ikon, dan package name yang berbeda per environment, gunakan flutter_flavorizr:

# pubspec.yaml -- hanya di dev_dependencies
dev_dependencies:
  flutter_flavorizr: ^2.2.3
# pubspec.yaml -- konfigurasi flavor
flavorizr:
  app:
    android:
      flavorDimensions: "version"
    ios: {}

  flavors:
    development:
      app:
        name: "[DEV] Toko Saya"
      android:
        applicationId: "com.contoh.tokosaya.dev"
        icon: "assets/icons/icon_dev.png"
      ios:
        bundleId: "com.contoh.tokosaya.dev"
        icon: "assets/icons/icon_dev.png"

    staging:
      app:
        name: "[STG] Toko Saya"
      android:
        applicationId: "com.contoh.tokosaya.staging"
        icon: "assets/icons/icon_staging.png"
      ios:
        bundleId: "com.contoh.tokosaya.staging"
        icon: "assets/icons/icon_staging.png"

    production:
      app:
        name: "Toko Saya"
      android:
        applicationId: "com.contoh.tokosaya"
        icon: "assets/icons/icon_production.png"
      ios:
        bundleId: "com.contoh.tokosaya"
        icon: "assets/icons/icon_production.png"
# Generate semua konfigurasi native
flutter pub run flutter_flavorizr

Entry Point Terpisah per Flavor #

Untuk perbedaan yang lebih besar antar environment (mock service, debug panel):

lib/
  main.dart              ← fallback / production
  main_development.dart  ← development entry point
  main_staging.dart      ← staging entry point
  main_production.dart   ← production entry point
// lib/main_development.dart
import 'package:flutter/material.dart';
import 'app.dart';
import 'core/config/app_config.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Development-specific setup
  if (AppConfig.isDevelopment) {
    // Gunakan mock HTTP client
    // Aktifkan debug panel
    // Nonaktifkan crash reporting
  }

  runApp(const App());
}
// lib/main_production.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'app.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const App());
}
# Jalankan dengan entry point tertentu
flutter run -t lib/main_development.dart
flutter build apk --release -t lib/main_production.dart

Konfigurasi VS Code dan Android Studio #

// .vscode/launch.json
{
  "configurations": [
    {
      "name": "Development",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_development.dart",
      "args": ["--dart-define-from-file=.dart_define/development.json"]
    },
    {
      "name": "Staging",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_staging.dart",
      "args": ["--dart-define-from-file=.dart_define/staging.json"]
    },
    {
      "name": "Production",
      "request": "launch",
      "type": "dart",
      "flutterMode": "release",
      "program": "lib/main_production.dart",
      "args": ["--dart-define-from-file=.dart_define/production.json"]
    }
  ]
}

Keamanan Konfigurasi #

JANGAN:
  ✗ Commit file .dart_define/*.json ke git (berisi API key!)
  ✗ Hardcode API key di kode sumber
  ✗ Simpan secret di pubspec.yaml

LAKUKAN:
  ✓ Tambahkan .dart_define/ ke .gitignore
  ✓ Simpan secret di environment variable CI/CD (GitHub Secrets)
  ✓ Buat file .dart_define/template.json tanpa nilai sensitif sebagai panduan

# .gitignore
.dart_define/
*.jks
*.p12
*.keystore
google-services.json
GoogleService-Info.plist

Ringkasan #

  • --dart-define adalah cara termudah untuk inject konfigurasi per environment tanpa setup native yang kompleks. Gunakan --dart-define-from-file dengan file JSON untuk konfigurasi yang lebih banyak.
  • AppConfig memusatkan semua konfigurasi dalam satu class — akses via AppConfig.apiUrl, AppConfig.isProduction, dll. Ini mencegah magic string yang tersebar.
  • Untuk perbedaan lebih dalam (nama app, ikon, package ID berbeda), gunakan flutter_flavorizr yang menggenerate konfigurasi Android/iOS secara otomatis.
  • Entry point terpisah (main_development.dart, main_production.dart) memungkinkan konfigurasi yang benar-benar berbeda per environment — mock service, debug panel, crash reporter.
  • Jangan commit file konfigurasi yang berisi API key dan secret ke git. Simpan di environment variable CI/CD dan generate saat build.
  • Konfigurasi VS Code launch.json dan Android Studio run configuration agar tim bisa dengan mudah berpindah antar environment dengan satu klik.

← Sebelumnya: Accessibility   Berikutnya: Build & Release →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact