In Dart, how do you read a value passed with Flutter's --dart-define, and why must String.fromEnvironment be invoked as const?
answer
- compile-time environment, not OS env
- const String.fromEnvironment('KEY')
- defaultValue is the empty string
- bool.hasEnvironment tells absent from empty
- a changed define needs a new run
basics
~20 sRead 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 sDefines 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 linesimport '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
Recall the reading pattern: const String.fromEnvironment('KEY', defaultValue: ...), plus the bool and int variants, and that a missing key gives the default.
Explain the compilation environment versus OS environment, why only const invocation is guaranteed on AOT builds, and what bool.hasEnvironment adds.
Validate required defines at startup, rely on const branches for tree shaking of mock backends, and keep secrets out of defines.
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