← Articles

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 strings on 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.

The right way to use --dart-define in Flutter — ANN Tech