The right way to use --dart-define in Flutter
By Charlin Joe · 5 March 2026
The --dart-define flag embeds compile-time values into your Flutter app. Used correctly, it's the cleanest way to manage environment-specific configuration without bundling secrets in your source code. Here is how to use it properly.
Basic usage
# Development
flutter run \
--dart-define=API_BASE_URL=http://localhost:3000 \
--dart-define=STRIPE_KEY=pk_test_xxx \
--dart-define=ENV=development
# Production build
flutter build apk \
--dart-define=API_BASE_URL=https://api.example.com \
--dart-define=STRIPE_KEY=pk_live_xxx \
--dart-define=ENV=production
Access in Dart:
// const = resolved at compile time, can be used anywhere a const is expected
const apiUrl = String.fromEnvironment('API_BASE_URL',
defaultValue: 'https://api.example.com',
);
const stripeKey = String.fromEnvironment('STRIPE_KEY');
const isDevelopment = String.fromEnvironment('ENV') == 'development';
const isProduction = bool.fromEnvironment('IS_PRODUCTION', defaultValue: true);
dart-define-from-file: the clean approach
Flutter 3.7+ added --dart-define-from-file which reads a JSON file:
// config/staging.json (never commit to git)
{
"API_BASE_URL": "https://api.staging.example.com",
"STRIPE_KEY": "pk_test_xxx",
"FIREBASE_PROJECT_ID": "myapp-staging",
"SENTRY_DSN": "https://...",
"ENV": "staging"
}
flutter run --dart-define-from-file=config/staging.json
flutter build appbundle --dart-define-from-file=config/production.json
Same Dart access: String.fromEnvironment('API_BASE_URL').
Configuration class
Don't scatter String.fromEnvironment calls throughout the app. Centralize:
// lib/core/config/app_config.dart
class AppConfig {
// All const — evaluated at compile time
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://api.example.com',
);
static const String stripePublishableKey = String.fromEnvironment(
'STRIPE_KEY',
);
static const String sentryDsn = String.fromEnvironment(
'SENTRY_DSN',
defaultValue: '',
);
static const bool isProduction = String.fromEnvironment(
'ENV',
defaultValue: 'production',
) == 'production';
static const bool isSentryEnabled = sentryDsn.isNotEmpty;
}
// Usage everywhere:
final dio = Dio(BaseOptions(baseUrl: AppConfig.apiBaseUrl));
Native platform access
dart-define values are also accessible in native code.
Android (build.gradle.kts):
val dartEnvironmentVariables: Map<String, String> = if (project.hasProperty('dart-defines')) {
// dart-defines is base64-encoded JSON
val decoded = String(Base64.getDecoder().decode(project.property('dart-defines') as String))
(Gson().fromJson(decoded, List::class.java) as List<String>)
.associate { it.split('=').let { parts -> parts[0] to parts[1] } }
} else emptyMap()
android {
buildTypes {
release {
buildConfigField(
"String",
"API_BASE_URL",
"\"${dartEnvironmentVariables["API_BASE_URL"] ?: ""}\""
)
}
}
}
iOS (in a Run Script Build Phase):
# ios/scripts/extract_dart_defines.sh
function entry_decode() { echo "${*}" | base64 --decode; }
IFS=',' read -r -a define_items <<< "$DART_DEFINES"
for item in "${define_items[@]}"; do
item="$(entry_decode "${item}")"
echo "$item" >> "${SRCROOT}/.env"
done
CI workflow
- name: Create config file
run: |
cat > config/production.json << 'EOF'
{
"API_BASE_URL": "${{ secrets.API_BASE_URL }}",
"STRIPE_KEY": "${{ secrets.STRIPE_KEY_PROD }}",
"SENTRY_DSN": "${{ secrets.SENTRY_DSN }}",
"ENV": "production"
}
EOF
- name: Build
run: |
flutter build appbundle \
--dart-define-from-file=config/production.json \
--build-number=${{ github.run_number }}
- name: Clean up secrets
run: rm config/production.json
if: always()
When NOT to use dart-define
- Values that change at runtime: dart-define is compile-time. For runtime feature flags, use Firebase Remote Config.
- Truly secret server keys: dart-define values are in the binary and can be extracted with tools like
stringson the IPA/APK. Don't put signing secrets or private keys in dart-define — proxy those calls through your backend. - Per-user configuration: not possible since it's compile-time.
Common pitfalls
Missing defaults. String.fromEnvironment('API_URL') returns empty string '' when not set. Always provide a meaningful default.
Confusing compile-time and runtime. You can't read dart-define values in isolates spawned at runtime — they're constants baked into the executable.
Not cleaning up config files in CI. Config files with secrets should be created from CI secrets and deleted after the build. The if: always() cleanup step ensures deletion even when the build fails.
Sign in to like, dislike, or report.