skip to content

In Dart, how do you read a value passed with Flutter's --dart-define, and why must String.fromEnvironment be invoked as const?

level: middleimportance: should knowfreq 45%

answer

  1. compile-time environment, not OS env
  2. const String.fromEnvironment('KEY')
  3. defaultValue is the empty string
  4. bool.hasEnvironment tells absent from empty
  5. a changed define needs a new run

basics

~20 s

Read it with const String.fromEnvironment('API_URL', defaultValue: ...), or bool.fromEnvironment and int.fromEnvironment. The value is fixed by the compiler, so only a const invocation is guaranteed to see it; ahead-of-time builds have no define table at runtime.

solid answer

~40 s

Defines passed with `--dart-define` or `--dart-define-from-file` form the **compilation environment**, not process environment variables. Dart reads them through `String.fromEnvironment`, `bool.fromEnvironment` and `int.fromEnvironment`; a missing key yields `defaultValue`, which is `''` for strings. The SDK documents that these constructors are only guaranteed to work when invoked as `const`: the compiler substitutes the value, while a non-const call may return the default on ahead-of-time targets such as release builds. `bool.hasEnvironment('API_URL')` tells an absent key from an empty one. Because the values are constants, `if (kUseMockBackend)` branches can be tree-shaken, but a changed define needs a new `flutter run` or build rather than a hot reload.

code

dart · 11 lines
dart
import 'package:flutter/material.dart';

const apiUrl = String.fromEnvironment('API_URL');
const useMockBackend = bool.fromEnvironment('USE_MOCK_BACKEND');

void main() {
  if (!useMockBackend && apiUrl.isEmpty) {
    throw StateError('API_URL is not set; pass --dart-define-from-file');
  }
  runApp(const TelemedicineApp());
}

go deeper

for a junior

Recall the reading pattern: const String.fromEnvironment('KEY', defaultValue: ...), plus the bool and int variants, and that a missing key gives the default.

for a middle

Explain the compilation environment versus OS environment, why only const invocation is guaranteed on AOT builds, and what bool.hasEnvironment adds.

for a senior

Validate required defines at startup, rely on const branches for tree shaking of mock backends, and keep secrets out of defines.

for a principal

Decide what belongs in build-time constants versus remotely fetched configuration, trading rebuild cost against runtime flexibility and exposure.

## Where the values come from When you run `flutter run --dart-define=API_URL=https://staging.api.example.com`, the Flutter tool hands the key-value pair to the Dart compiler as part of the **compilation configuration environment**. It is not the operating system's environment: `Platform.environment` from `dart:io` will not contain `API_URL`, and on a phone the build machine's variables do not exist at all. The same applies to `--dart-define-from-file=config/staging.json` (or a `.env` file): every top-level key becomes an entry in that environment, and a `--dart-define` with the same key overrides the file. ## Reading them Dart provides three constructors and one test: ```dart const apiUrl = String.fromEnvironment('API_URL', defaultValue: 'https://api.example.com'); const useMocks = bool.fromEnvironment('USE_MOCKS'); // default false const retryCount = int.fromEnvironment('RETRY_COUNT', defaultValue: 3); const hasApiUrl = bool.hasEnvironment('API_URL'); ``` Defaults when the key is absent: - `String.fromEnvironment` returns `defaultValue`, which itself defaults to `''`. - `bool.fromEnvironment` returns `defaultValue`, which defaults to `false`. - `int.fromEnvironment` returns `defaultValue`, which defaults to `0`. - `bool.hasEnvironment` returns whether the key was declared, so you can tell "not passed" apart from "passed as empty". ## Why `const` matters The SDK's documentation for `String.fromEnvironment` says the constructor is **only guaranteed to work when invoked as `const`**. It may work as a non-constant call on platforms that have compiler options at run time, but most **ahead-of-time compiled** platforms do not. A Flutter release build on Android or iOS is AOT-compiled, so: ```dart final apiUrl = String.fromEnvironment('API_URL'); // wrong: not const ``` may appear to work in a debug session and then yield the empty default in a release build. Declaring it `const` makes the compiler evaluate it at compile time, which is exactly when the define exists. ## Consequences of being a compile-time constant 1. **Tree shaking.** With `const useMocks = bool.fromEnvironment('USE_MOCKS');`, a branch like `if (useMocks) { ... }` is known at compile time, so the unused branch — and code reachable only from it — can be removed from the release binary. 2. **Hot reload does not pick up a new value.** Defines are fixed for the whole `flutter run` session; changing one means stopping and running again. 3. **The value is in the binary.** A constant string ends up in the compiled app and can be extracted. Defines are for configuration such as API URLs and feature switches, not for secrets. 4. **Validation belongs at startup.** Because a missing key silently becomes `''`, fail fast in `main()`: ```dart void main() { assert(apiUrl.isNotEmpty, 'API_URL missing: pass --dart-define-from-file'); if (apiUrl.isEmpty) throw StateError('API_URL is not configured'); runApp(const TelemedicineApp()); } ``` ## Define files in practice `--dart-define-from-file` keeps a build's configuration in one reviewable file instead of a long command line: - The file may be a **JSON object** or a **`.env`-style** list of `KEY=value` lines; the tool treats content starting with `{` as JSON and converts anything else from `.env` format. - The option can be **repeated**, so a shared `config/common.json` can be combined with `config/staging.json`. - An explicit **`--dart-define` wins** over a file entry with the same key, which is how a developer points one run at a local server without editing the file. - Multi-line values in a `.env` file are rejected by the tool with an error rather than silently truncated. - Keys are case-sensitive and must match the `fromEnvironment` call exactly; a typo is not an error, it is just an empty default, which is why startup validation matters. ## `appFlavor` is the same mechanism The framework's `appFlavor` constant is itself declared with `String.fromEnvironment('FLUTTER_APP_FLAVOR')`, which the Flutter tool fills in from `--flavor`. That is why `appFlavor` is a `const String?` and why the tool refuses a `--dart-define` that tries to set `FLUTTER_APP_FLAVOR`. | Call | Absent key gives | Typical use | |---|---|---| | `String.fromEnvironment(k)` | `''` | URLs, names | | `bool.fromEnvironment(k)` | `false` | feature switches | | `int.fromEnvironment(k)` | `0` | numeric limits | | `bool.hasEnvironment(k)` | `false` | presence checks |

  • Why does Platform.environment['API_URL'] return null in a Flutter app built with --dart-define=API_URL=...?
    `Platform.environment` reads the running process's operating-system environment on the device. `--dart-define` feeds the compiler's configuration environment instead, which is only readable through the `fromEnvironment` constructors, invoked as `const`.
  • What file formats does --dart-define-from-file accept?
    A JSON object or a `.env`-style file of `KEY=value` lines; the tool treats content starting with `{` as JSON and converts the rest from `.env`. The option can be repeated, and `--dart-define` entries with the same key take precedence over file entries.

saying these in an interview costs you the question

  • Reads defines with Platform.environment from dart:io
  • Calls String.fromEnvironment without const and trusts the release build
  • Expects hot reload to pick up a changed --dart-define value
  • Assumes a missing define throws instead of returning the default
  • Stores API secrets in dart-defines believing they stay out of the binary