Flutter Flavors and Environment Config
Professional apps run in several environments. Developers test against a development server full of fake data. Testers check a staging server that mirrors production. Real users connect to the production server. Each environment needs its own web address, settings, and sometimes its own app name and icon. Flavors and build-time configuration let one codebase serve all of them.
Why Separate Environments
Developer phone ──→ DEV server (fake data, safe to break)
Tester phone ──→ STAGING server (copy of real setup)
User phone ──→ PRODUCTION (real data, must stay stable)
Running tests against production risks corrupting real records. Separate environments let teams experiment freely and release with confidence.
Approach 1: dart-define Values
The simplest method passes values when you launch the app.
flutter run --dart-define=ENV=dev --dart-define=API_URL=https://dev.example.com
Read them in Dart as compile-time constants:
class Config {
static const String env = String.fromEnvironment('ENV', defaultValue: 'dev');
static const String apiUrl = String.fromEnvironment(
'API_URL',
defaultValue: 'https://dev.example.com',
);
static bool get isProduction => env == 'prod';
}
Use Config.apiUrl when you create the Dio client. Change the command line, and the whole app points elsewhere.
Storing Many Values in a File
A long list of defines is tiring to type. Put them in a JSON file and pass the file.
// config/dev.json
{
"ENV": "dev",
"API_URL": "https://dev.example.com"
}
flutter run --dart-define-from-file=config/dev.json
Approach 2: Separate Entry Points
Create one main file per environment. Each file sets its configuration and starts the same app.
lib/
├── main_dev.dart
├── main_staging.dart
├── main_prod.dart
└── app.dart
// main_dev.dart
import 'app.dart';
void main() {
AppConfig.apiUrl = 'https://dev.example.com';
AppConfig.name = 'MyApp Dev';
runApp(MyApp());
}
flutter run -t lib/main_dev.dart
Approach 3: Native Flavors
Flavors go further. They change the native build, so each environment installs as a separate app on the same phone with its own name, icon, and package id. A tester keeps the development and production versions side by side.
Android
Add flavors in android/app/build.gradle:
android {
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationIdSuffix ".dev"
resValue "string", "app_name", "MyApp Dev"
}
prod {
dimension "env"
resValue "string", "app_name", "MyApp"
}
}
}
Run it with flutter run --flavor dev -t lib/main_dev.dart.
iOS
iOS uses schemes and build configurations in Xcode. Create a scheme per environment and match its name to the flavor name. Packages such as flutter_flavorizr automate both platform setups when you want less manual work.
Choosing a Method
| Method | Effort | Best For |
|---|---|---|
| dart-define | Low | Different URLs and feature switches |
| Separate entry points | Low | Different startup code per environment |
| Native flavors | High | Different app names, icons, and ids |
Secrets Do Not Belong in the App
A published app is a package that anyone can open. Strings stored in Dart code, in a JSON config, or in an .env file bundled as an asset can be extracted. Values passed through dart-define are obfuscated only lightly. Treat everything inside the app as public.
- Never embed private server keys, database passwords, or payment secrets in the app.
- Keep sensitive operations on your server and let the app call that server.
- Use public client keys only where the provider designs them for client use, and restrict them by app id.
- Store user tokens at runtime in secure storage, not in code.
Feature Flags
Config values can switch features on or off. A flag such as ENABLE_BETA_SEARCH exposes a new feature in development and staging while hiding it in production until the feature is ready.
static const bool betaSearch = bool.fromEnvironment('BETA_SEARCH', defaultValue: false);
if (Config.betaSearch) {
// show the new search bar
}
Showing the Environment
Testers confuse builds easily. Show a small banner such as "DEV" in non-production builds, and the confusion disappears. Flutter's Banner widget draws a corner ribbon with one line of code.
Practice Ideas
Create a Config class and run the app with two different API_URL values. Print the active environment on the home screen. Add a corner banner for every non-production build, and verify that a production run hides it.
