Flavors & Environment #
Dalam siklus pengembangan aplikasi seluler profesional, kita selalu membutuhkan lebih dari satu lingkungan kerja (environment). Idealnya, minimal terdapat tiga lingkungan kerja yang terpisah:
- Development (Dev): Lingkungan yang digunakan oleh pengembang untuk bereksperimen, menulis fitur baru, mengaktifkan log debug secara agresif, dan menguji kode terhadap database lokal atau server pengembangan.
- Staging (Stg) / QA: Lingkungan yang mereplikasi kondisi produksi secara presisi. Digunakan oleh tim penjamin mutu (Quality Assurance) untuk menguji fungsionalitas aplikasi, menguji integrasi API eksternal pihak ketiga, serta melakukan uji performa sebelum kode didorong ke tahap perilisan.
- Production (Prod): Lingkungan rilis final yang diunduh dan digunakan oleh pengguna riil di App Store atau Google Play Store. Keamanan, kerahasiaan kunci API, dan efisiensi performa sangat diutamakan di lingkungan ini.
Pemisahan lingkungan ini menjamin bahwa aktivitas pengembangan atau pengujian tidak akan mencemari data transaksi pada database produksi, serta memastikan kunci API rahasia produksi tidak terekspos secara tidak sengaja.
Arsitektur Pemisahan Environment pada Aplikasi Seluler #
Dalam Flutter, pemisahan lingkungan kerja dilakukan pada dua tingkat arsitektur yang berbeda:
- Tingkat Dart (Dart-Level Configuration): Mengatur variabel yang hanya relevan bagi kode Dart kita, seperti endpoint URL API gateway, durasi timeout koneksi jaringan, atau flag boolean untuk mengaktifkan log debug dan mock service. Kita memanfaatkan parameter
--dart-define-from-fileuntuk menyuntikkan variabel-variabel ini pada saat kompilasi. - Tingkat Platform Native (Native-Level Flavors): Mengatur konfigurasi yang tertanam di dalam sistem operasi target. Hal ini mencakup perbedaan Nama Aplikasi (misalnya
[DEV] Toko KitavsToko Kita), Identitas Paket Aplikasi (Application ID di Android dan Bundle ID di iOS), ikon launcher aplikasi yang berbeda agar tidak membingungkan penguji, serta berkas integrasi SDK native (sepertigoogle-services.jsonmilik Firebase).
Dengan mengonfigurasi native flavor, kita dapat memasang (install) aplikasi versi Dev, Staging, dan Prod secara berdampingan pada satu perangkat fisik yang sama sekaligus.
Berikut adalah diagram alur kompilasi aplikasi Flutter berdasarkan kombinasi flavor native dan injeksi variabel Dart:
flowchart TD
BuildTrigger(["Mulai Kompilasi (flutter run / build)"]) --> SelectConfig{"Pilih Environment & Target"}
SelectConfig -- "--flavor development" --> AndroidDev["Android: Product Flavor 'development'<br/>Application ID: com.app.dev<br/>Ikon: icon_dev.png"]
SelectConfig -- "--flavor production" --> AndroidProd["Android: Product Flavor 'production'<br/>Application ID: com.app<br/>Ikon: icon_prod.png"]
SelectConfig -- "--dart-define-from-file" --> DartInject["Dart Compiler:<br/>Membaca JSON Config<br/>Menginjeksikan konstanta via VM"]
AndroidDev --> CompileNative["Native Compiler (Gradle / Xcode)"]
AndroidProd --> CompileNative
DartInject --> CompileNative
CompileNative --> FinalOutput["Biner Akhir (APK/AAB/IPA)<br/>Endpoint API: dev/prod URL<br/>Nama Aplikasi: [DEV] Nama / Nama<br/>Dapat diinstal berdampingan"]Pendekatan 1: Konfigurasi Dart-Level via –dart-define-from-file #
Flutter modern menyediakan parameter --dart-define-from-file yang memungkinkan kita menyuntikkan kumpulan variabel lingkungan berbasis file JSON eksternal pada waktu kompilasi. Ini jauh lebih bersih dan mudah dikelola dibandingkan menulis puluhan parameter --dart-define secara manual di terminal command line.
1. Membuat Berkas Konfigurasi JSON #
Buat direktori baru bernama .dart_define/ di root proyek kita (pastikan direktori ini dimasukkan ke dalam .gitignore). Buat tiga file konfigurasi terpisah:
// .dart_define/development.json
{
"APP_ENV": "development",
"API_URL": "https://api-dev.unisbadri.com/v1",
"API_KEY": "key_dev_abcsystem123",
"ENABLE_LOGS": "true",
"TIMEOUT_DETIK": "10"
}
// .dart_define/staging.json
{
"APP_ENV": "staging",
"API_URL": "https://api-stg.unisbadri.com/v1",
"API_KEY": "key_stg_xyztesting456",
"ENABLE_LOGS": "true",
"TIMEOUT_DETIK": "15"
}
// .dart_define/production.json
{
"APP_ENV": "production",
"API_URL": "https://api.unisbadri.com/v1",
"API_KEY": "key_prod_securedsystem789",
"ENABLE_LOGS": "false",
"TIMEOUT_DETIK": "30"
}
2. Implementasi Kelas AppConfig bertipe Aman (Type-Safe) #
Di dalam kode Dart, kita membaca variabel-variabel tersebut menggunakan metode String.fromEnvironment, bool.fromEnvironment, dan int.fromEnvironment. Kita memusatkan akses variabel ini di dalam sebuah kelas konfigurasi terpadu.
// lib/core/config/app_config.dart
enum EnvironmentType { development, staging, production }
class AppConfig {
// 1. Baca data mentah dari environment compile-time
static const String _env = String.fromEnvironment(
'APP_ENV',
defaultValue: 'development',
);
static const String apiUrl = String.fromEnvironment(
'API_URL',
defaultValue: 'https://api-dev.unisbadri.com/v1',
);
static const String apiKey = String.fromEnvironment(
'API_KEY',
defaultValue: '',
);
static const bool enableLogs = bool.fromEnvironment(
'ENABLE_LOGS',
defaultValue: true,
);
static const int timeoutDetik = int.fromEnvironment(
'TIMEOUT_DETIK',
defaultValue: 10,
);
// 2. Petakan string menjadi tipe enum aman
static EnvironmentType get environment {
switch (_env) {
case 'staging':
return EnvironmentType.staging;
case 'production':
return EnvironmentType.production;
default:
return EnvironmentType.development;
}
}
static bool get isDevelopment => environment == EnvironmentType.development;
static bool get isStaging => environment == EnvironmentType.staging;
static bool get isProduction => environment == EnvironmentType.production;
/// Contoh parameter dinamis yang nilainya ditentukan berdasarkan environment
static Duration get connectionTimeout => Duration(seconds: timeoutDetik);
}
Untuk menjalankan aplikasi menggunakan konfigurasi ini di terminal, gunakan perintah:
# Jalankan di lingkungan pengembangan
flutter run --dart-define-from-file=.dart_define/development.json
# Bangun biner rilis produksi
flutter build apk --release --dart-define-from-file=.dart_define/production.json
Pendekatan 2: Konfigurasi Native-Level via Product Flavors & Schemes #
Jika kita ingin mengubah identitas paket aplikasi (application ID) dan nama aplikasi di layar HP agar aplikasi Dev, Staging, dan Prod dapat terinstal berdampingan, kita wajib melakukan konfigurasi di level native platform.
1. Setup Android Product Flavors #
Buka file android/app/build.gradle (bukan root build.gradle), lalu tambahkan blok konfigurasi flavorDimensions dan productFlavors di dalam blok android { ... }:
android {
...
defaultConfig {
applicationId "com.unisbadri.tokokita"
minSdkVersion flutterMinSdkVersion
targetSdkVersion flutterTargetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
// 1. Tentukan dimensi rilis
flavorDimensions "default"
// 2. Definisikan konfigurasi tiap flavor
productFlavors {
development {
dimension "default"
applicationIdSuffix ".dev" // Menghasilkan com.unisbadri.tokokita.dev
resValue "string", "app_name", "[DEV] Toko Kita"
}
staging {
dimension "default"
applicationIdSuffix ".staging" // Menghasilkan com.unisbadri.tokokita.staging
resValue "string", "app_name", "[STG] Toko Kita"
}
production {
dimension "default"
// Menggunakan applicationId default tanpa suffix (com.unisbadri.tokokita)
resValue "string", "app_name", "Toko Kita"
}
}
}
Buka file android/app/src/main/AndroidManifest.xml, cari bagian tag <application> dan ganti nilai atribut android:label agar membaca nilai string dinamis dari flavor kita:
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
...>
2. Setup iOS Schemes & Configurations #
Pada iOS, kita menduplikasi konfigurasi rilis bawaan Xcode (Debug, Release, Profile) menjadi berorientasi flavor:
- Buka proyek iOS di Xcode (
open ios/Runner.xcworkspace). - Pilih proyek Runner di panel kiri $\rightarrow$ tab Info $\rightarrow$ cari bagian Configurations.
- Duplikasikan konfigurasi yang ada dengan mengklik ikon +:
- Duplikasikan
Debugmenjadi:Debug-development,Debug-staging,Debug-production. - Duplikasikan
Releasemenjadi:Release-development,Release-staging,Release-production. - Duplikasikan
Profilemenjadi:Profile-development,Profile-staging,Profile-production.
- Duplikasikan
- Pilih Build Settings $\rightarrow$ cari Product Bundle Identifier. Sesuaikan ID paket untuk masing-masing konfigurasi:
- Untuk semua konfigurasi
*-development, isi:com.unisbadri.tokokita.dev - Untuk semua konfigurasi
*-staging, isi:com.unisbadri.tokokita.staging - Untuk semua konfigurasi
*-production, isi:com.unisbadri.tokokita
- Untuk semua konfigurasi
- Untuk mengubah nama aplikasi secara dinamis, tambahkan konfigurasi kustom (User-Defined Setting) bernama
APP_DISPLAY_NAMEdi Xcode:*-development$\rightarrow$[DEV] Toko Kita*-staging$\rightarrow$[STG] Toko Kita*-production$\rightarrow$Toko Kita
- Buka berkas
ios/Runner/Info.plist, ubah tagCFBundleDisplayNamedanCFBundleNameagar membaca variabel kustom tersebut:
<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>
<key>CFBundleName</key>
<string>$(APP_DISPLAY_NAME)</string>
- Buat skema baru (Xcode Schemes) untuk masing-masing environment (
development,staging,production) dan hubungkan aksi target build-nya ke konfigurasi yang sesuai.
Setup Ikon Launcher Berbeda per Flavor #
Penguji aplikasi (QA team) akan sangat terbantu jika ikon aplikasi di layar ponsel mereka memiliki penanda visual yang jelas (misalnya pita banner bertuliskan “DEV” atau “STG”) agar tidak tertukar saat menguji.
Kita dapat mengotomatiskan pembuatan ikon peluncur (launcher icons) untuk masing-masing flavor menggunakan package flutter_launcher_icons.
1. Konfigurasi pubspec.yaml #
Tambahkan konfigurasi pembuatan ikon kustom per flavor di bawah blok dev_dependencies berkas pubspec.yaml kita:
dev_dependencies:
flutter_launcher_icons: ^0.13.1
# Konfigurasi ikon per flavor
flutter_launcher_icons:
development:
image_path: "assets/icons/launcher_dev.png"
android: true
ios: true
staging:
image_path: "assets/icons/launcher_stg.png"
android: true
ios: true
production:
image_path: "assets/icons/launcher_prod.png"
android: true
ios: true
2. Jalankan Perintah Generator Ikon #
Jalankan perintah berikut di konsol terminal untuk menginstruksikan generator membuat aset gambar native secara otomatis ke folder res Android dan Assets iOS:
flutter pub run flutter_launcher_icons:main
Manajemen Entry Point Terpisah per Environment #
Untuk memisahkan logika inisialisasi aplikasi secara bersih (misalnya mengaktifkan pelaporan error Firebase Crashlytics hanya di lingkungan produksi, atau menggunakan basis data tiruan di lingkungan pengembangan), kita disarankan menggunakan berkas entry point (main) yang terpisah.
Kita akan membagi file entry point menjadi:
lib/main_development.dartlib/main_staging.dartlib/main_production.dartlib/main_common.dart(Berisi fungsi inisialisasi bersama)
1. Implementasi lib/main_common.dart #
// lib/main_common.dart
import 'package:flutter/material.dart';
import 'core/config/app_config.dart';
/// Fungsi runner bersama yang dipanggil oleh masing-masing entry point flavor
void jalankanAplikasiBersama() async {
WidgetsFlutterBinding.ensureInitialized();
// Logika inisialisasi bersama
debugPrint('Menjalankan aplikasi pada mode environment: ${AppConfig.environment}');
runApp(const AplikasiUtama());
}
class AplikasiUtama extends StatelessWidget {
const AplikasiUtama({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text(AppConfig.isProduction ? 'Toko Kita' : '[DEBUG] Toko Kita')),
body: Center(
child: Text('API Gateway: ${AppConfig.apiUrl}'),
),
),
);
}
}
2. Implementasi lib/main_development.dart #
// lib/main_development.dart
import 'main_common.dart';
void main() {
// Di sini kita dapat mendaftarkan dependensi mock khusus development
// sebelum memanggil runner bersama
jalankanAplikasiBersama();
}
3. Implementasi lib/main_production.dart #
// lib/main_production.dart
import 'main_common.dart';
void main() async {
// Di sini kita mengaktifkan logging produksi, menginisialisasi crash reporting,
// atau melakukan validasi sertifikat sebelum menjalankan aplikasi
jalankanAplikasiBersama();
}
Untuk menjalankan atau mem-build rilis menggunakan entry point dan flavor spesifik ini, gunakan parameter -t (target) dan --flavor:
# Menjalankan versi development
flutter run -t lib/main_development.dart --flavor development --dart-define-from-file=.dart_define/development.json
# Mem-build berkas rilis produksi AAB
flutter build appbundle -t lib/main_production.dart --flavor production --dart-define-from-file=.dart_define/production.json
Konfigurasi IDE (VS Code & Android Studio) #
Agar seluruh anggota tim pengembang kita dapat menjalankan aplikasi pada berbagai environment dengan mudah tanpa harus mengetik perintah CLI yang panjang di terminal, kita harus menyediakan konfigurasi peluncuran (launch configuration) pada editor IDE.
1. Konfigurasi untuk VS Code #
Buat berkas .vscode/launch.json di dalam folder proyek kita:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Development (Dev)",
"request": "launch",
"type": "dart",
"program": "lib/main_development.dart",
"args": [
"--flavor",
"development",
"--dart-define-from-file",
".dart_define/development.json"
]
},
{
"name": "Run Staging (Stg)",
"request": "launch",
"type": "dart",
"program": "lib/main_staging.dart",
"args": [
"--flavor",
"staging",
"--dart-define-from-file",
".dart_define/staging.json"
]
},
{
"name": "Run Production (Prod)",
"request": "launch",
"type": "dart",
"program": "lib/main_production.dart",
"args": [
"--flavor",
"production",
"--dart-define-from-file",
".dart_define/production.json"
]
}
]
}
Kini pengembang cukup menekan tombol F5 di VS Code dan memilih konfigurasi target dari menu drop-down untuk mulai melakukan debugging.
Keamanan Konfigurasi & Manajemen Kunci Rahasia #
Kunci API (API keys), kunci enkripsi, dan token kredensial pihak ketiga yang dimasukkan ke dalam berkas konfigurasi tidak boleh terekspos ke publik.
Berikut adalah panduan ketat untuk mengamankan data konfigurasi kita:
- Tambahkan ke gitignore: Pastikan folder
.dart_define/dan berkas kredensial native dimasukkan ke dalam.gitignoreproyek kita agar tidak pernah terunggah ke repositori git publik.# .gitignore .dart_define/ android/app/*.keystore android/app/*.jks ios/Runner/*.p12 ios/Runner/google-services.json ios/Runner/GoogleService-Info.plist - Buat Berkas Template Konfigurasi: Sebagai panduan bagi pengembang lain yang baru bergabung di proyek kita untuk menyusun file definisinya sendiri, buat file contoh tanpa nilai kunci riil di bawah pengawasan Git.
// .dart_define/development.json.example { "APP_ENV": "development", "API_URL": "https://api-dev.example.com", "API_KEY": "MASUKKAN_KEY_PENGEMBANGAN_DISINI", "ENABLE_LOGS": "true", "TIMEOUT_DETIK": "10" } - Gunakan GitHub Secrets pada CI/CD: Saat memproses build otomatis di server CI/CD (seperti GitHub Actions), jangan simpan berkas JSON konfigurasi di repositori. Simpan nilai rahasia di menu pengaturan enkripsi rahasia penyedia CI/CD kita, dan gunakan skrip shell untuk menulis (generate) file JSON secara dinamis sesaat sebelum kompilasi build dimulai.
Ringkasan #
- Pemisahan Environment: Lingkungan Dev, Staging, dan Prod harus dipisahkan untuk menjaga integritas data produksi dan kerahasiaan kredensial keamanan sistem kita.
- –dart-define-from-file: Manfaatkan file konfigurasi JSON compile-time untuk menyuntikkan data konfigurasi URL API gateway dan variabel konfigurasi lainnya ke kode Dart secara rapi.
- Native Product Flavors: Konfigurasikan native flavor di Android (
build.gradle) dan Schemes di iOS (Xcode) untuk membedakan nama aplikasi, Application ID/Bundle ID, dan membolehkan instalasi berdampingan.- Entry Point Terpisah: Gunakan file entri berbeda (
main_development.dart,main_production.dart) guna mengisolasi inisialisasi dependensi mock dari kode produksi rilis secara aman.- Keamanan Repositori: Selalu pastikan folder
.dart_define/dan file penandatanganan native (.keystore,.jks,.p12) masuk ke dalam.gitignoredemi menghindari kebocoran kredensial ke publik.