Managing Environment Configurations with .env and Dart Defines
Managing Environment Configurations with Dart Defines
Every production Flutter app needs to handle multiple environments: development, staging, and production. Each environment has different API base URLs, feature flags, analytics keys, and third-party credentials. Hardcoding these values directly in your source code creates two serious problems: secrets end up in version control, and switching environments requires code changes.
Flutter solves this with --dart-define and --dart-define-from-file, which inject values at build time as compile-time constants. These values never live in your Dart source files; they are passed on the command line or loaded from a local file that is gitignored.
Passing Values with --dart-define
The simplest approach is to pass key-value pairs directly on the flutter build or flutter run command line:
Passing defines on the command line
# Development run
flutter run \
--dart-define=API_BASE_URL=https://dev-api.example.com \
--dart-define=ENABLE_ANALYTICS=false \
--dart-define=MAPS_API_KEY=dev_key_abc123
# Production build
flutter build apk --release \
--dart-define=API_BASE_URL=https://api.example.com \
--dart-define=ENABLE_ANALYTICS=true \
--dart-define=MAPS_API_KEY=prod_key_xyz789
Inside your Dart code, read these constants using String.fromEnvironment(), bool.fromEnvironment(), or int.fromEnvironment(). These are compile-time constant constructors, so they must be assigned to const variables:
Reading dart-define values in Dart code
// lib/config/app_config.dart
class AppConfig {
// String value — defaults to empty string if not provided
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8080',
);
// Boolean flag — defaults to false
static const bool enableAnalytics = bool.fromEnvironment(
'ENABLE_ANALYTICS',
defaultValue: false,
);
// Integer value — defaults to 30
static const int requestTimeoutSeconds = int.fromEnvironment(
'REQUEST_TIMEOUT_SECONDS',
defaultValue: 30,
);
// Third-party API key — empty by default
static const String mapsApiKey = String.fromEnvironment(
'MAPS_API_KEY',
defaultValue: '',
);
}
// Usage anywhere in the app:
// final dio = Dio(BaseOptions(baseUrl: AppConfig.apiBaseUrl));
// if (AppConfig.enableAnalytics) FirebaseAnalytics.instance.logEvent(...);
Using --dart-define-from-file
Passing many values on the command line becomes unwieldy. Flutter 3.7+ supports --dart-define-from-file, which reads all key-value pairs from a JSON file. You create one JSON file per environment, add them all to .gitignore, and only commit a .env.example or documentation file describing the expected keys.
.env.dev.json (not committed to git)
{
"API_BASE_URL": "https://dev-api.example.com",
"ENABLE_ANALYTICS": "false",
"MAPS_API_KEY": "dev_key_abc123",
"REQUEST_TIMEOUT_SECONDS": "30",
"SENTRY_DSN": ""
}
Running with a define file
# Development
flutter run --dart-define-from-file=.env.dev.json
# Staging
flutter build apk --dart-define-from-file=.env.staging.json
# Production release
flutter build appbundle --release \
--dart-define-from-file=.env.prod.json
.env.example.json to version control that lists all required keys with placeholder values. This documents what each developer needs to create locally without exposing real secrets. Include a comment in your README explaining that each developer must copy this file and fill in real values.Structuring Your Config Class
A well-structured config class consolidates all environment access into one place, making it easy to audit what values an environment must supply and to swap out implementations during testing:
Complete AppConfig with validation
// lib/config/app_config.dart
class AppConfig {
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8080',
);
static const String sentryDsn = String.fromEnvironment('SENTRY_DSN');
static const bool enableAnalytics = bool.fromEnvironment(
'ENABLE_ANALYTICS',
defaultValue: false,
);
static const bool enableCrashReporting = bool.fromEnvironment(
'ENABLE_CRASH_REPORTING',
defaultValue: false,
);
static const String mapsApiKey = String.fromEnvironment('MAPS_API_KEY');
/// Call this at app startup to fail fast if required values are missing.
static void validate() {
if (apiBaseUrl.isEmpty) {
throw StateError('API_BASE_URL must be set via --dart-define');
}
if (mapsApiKey.isEmpty) {
throw StateError('MAPS_API_KEY must be set via --dart-define');
}
}
}
// In main():
// void main() {
// AppConfig.validate();
// runApp(const MyApp());
// }
gitignore Best Practices
Add all real environment files to .gitignore to prevent secrets from being committed:
.gitignore entries for environment files
# Environment config files (contain real secrets)
.env.dev.json
.env.staging.json
.env.prod.json
# Keep the example file tracked
# .env.example.json <-- do NOT ignore this one
--dart-define arguments in a CI/CD pipeline script that is committed to version control. Instead, store secrets as CI/CD environment variables (GitHub Actions secrets, GitLab CI variables, Bitrise secrets) and expand them at build time: flutter build apk --dart-define=MAPS_API_KEY=$MAPS_API_KEY.Native Platform Access
Dart defines are also accessible inside native platform configuration files. For Android (build.gradle) and iOS (ExportOptions.plist / xcconfig), Flutter propagates dart-define values so you can configure platform-specific settings like the Google Maps SDK key in AndroidManifest.xml:
Reading dart-define in android/app/build.gradle
// In android/app/build.gradle
def dartDefines = [:]
if (project.hasProperty('dart-defines')) {
project.property('dart-defines').split(',').each { entry ->
def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
if (pair.length == 2) dartDefines[pair[0]] = pair[1]
}
}
android {
defaultConfig {
manifestPlaceholders += [
mapsApiKey: dartDefines.getOrDefault('MAPS_API_KEY', '')
]
}
}
Summary
Using --dart-define and --dart-define-from-file keeps environment-specific values — API URLs, feature flags, and secret keys — out of your source code. Values are injected at build time as compile-time constants, read in Dart with String.fromEnvironment(), and can also propagate to native platform build files. Always add real environment JSON files to .gitignore, commit only an example template, and store production secrets in your CI/CD platform's secret store.