skip to content

For a Flutter telemedicine app with staging and production, would you use separate entry points with -t, or one main.dart driven by appFlavor and define files, and why?

level: seniorimportance: should knowfreq 30%

answer

  1. --target picks the Dart entry file
  2. entry points duplicate wiring
  3. one main plus define files
  4. flavor and config can mismatch
  5. validate the pair at startup

basics

~20 s

Prefer one main.dart that reads appFlavor and const defines from a per-environment file, validated at startup; separate main_staging.dart entry points chosen with -t work, but duplicate wiring and add a third switch that can disagree with --flavor.

solid answer

~40 s

`flutter run -t lib/main_staging.dart` (`--target`) selects a different Dart entry file; it changes nothing native. Entry points per environment were a common pattern: each `main_*.dart` builds a config object and calls a shared `bootstrap`. They work, but a build is then described by three switches — `--flavor`, `-t` and the define file — and nothing forces them to agree, so a production-flavored app can ship pointing at staging. A single `main.dart` that resolves configuration from `appFlavor` and `const String.fromEnvironment` values, validates that they match, and injects one config object keeps one code path and moves the difference into data. Entry points still earn their place when environments differ in code, such as a mock backend that must not ship in production.

code

bash · 5 lines
bash
# Design A: entry point per environment
flutter run --flavor staging -t lib/main_staging.dart

# Design B: one main.dart, values from a define file
flutter run --flavor staging --dart-define-from-file=config/staging.json

go deeper

for a junior

Know that -t selects which Dart file holds main(), while --flavor selects the native variant; they are independent switches.

for a middle

Compare where values live in each design and how const defines and appFlavor feed a single configuration object.

for a senior

Argue from the mismatched-build failure: fewer switches, scripted builds and startup validation, with entry points kept for code-level differences.

for a principal

Set the standard for the codebase: one source of environment truth, how new environments are added, and how builds are reproduced in CI.

## The two designs A Flutter app with **staging** and **production** environments has to decide where environment differences live. Two designs are common in interviews and in real codebases. **Design A: one entry point per environment.** ```text lib/main_staging.dart -> bootstrap(AppConfig.staging()) lib/main_production.dart -> bootstrap(AppConfig.production()) ``` Each file is selected with `--target` (short `-t`): `flutter run --flavor staging -t lib/main_staging.dart`. `--target` only chooses which Dart file contains `main()`; it does nothing to the Android or Xcode project. **Design B: one `main.dart`, configuration as data.** ```text lib/main.dart -> bootstrap(AppConfig.fromEnvironment()) config/staging.json -> API_URL, ENVIRONMENT, feature flags config/production.json ``` Selected with `flutter run --flavor staging --dart-define-from-file=config/staging.json`. `AppConfig.fromEnvironment()` reads `appFlavor` and `const String.fromEnvironment(...)` values. ## Trade-offs | Concern | Separate entry points (`-t`) | Single `main.dart` + defines | |---|---|---| | Switches per build | `--flavor`, `-t`, often defines | `--flavor`, define file | | Where values live | Dart code per file | JSON or `.env` files | | Code that differs per environment | natural (different imports) | needs `const` branches | | Risk of mismatched builds | higher (three switches) | lower, and checkable | | Keeping mock code out of production | easy (never imported) | relies on tree shaking of `const` branches | | Onboarding and IDE configs | one launch config per file | one config per flavor | ## The failure that decides it The costly incident in this area is a **mismatched build**: a production application ID talking to the staging backend, or the reverse. For a telemedicine app that means real patients' sessions hitting a test server, or testers creating records in production. With Design A, `--flavor production -t lib/main_staging.dart` is a perfectly valid command. With Design B the same mistake is `--flavor production --dart-define-from-file=config/staging.json`, but it can be **detected**, because both facts are visible in one place at startup: ```dart const apiUrl = String.fromEnvironment('API_URL'); const environment = String.fromEnvironment('ENVIRONMENT'); void main() { if (appFlavor != null && appFlavor != environment) { throw StateError('flavor $appFlavor built with $environment config'); } runApp(TelemedicineApp(config: AppConfig(apiUrl: apiUrl))); } ``` ## When separate entry points still make sense 1. **Environments differ in code, not just values.** A demo build wired to an in-memory fake backend should not even import that fake in production. A separate entry point guarantees it is unreachable; a `const bool.fromEnvironment` branch achieves the same only if every reference sits behind the constant. 2. **Development-only tooling.** A `main_dev.dart` that installs a debug overlay or a network inspector keeps that tooling out of the release entry point entirely. 3. **Migration.** A large app already built around `main_*.dart` files can keep them as thin shims that only choose a config and call the shared `bootstrap`, while values move into define files. ## A shared bootstrap either way Whichever design you choose, keep the entry point thin. Everything after "which configuration?" — dependency injection, error reporting setup, `runApp` — lives in one `bootstrap(AppConfig config)` function: ```dart Future<void> bootstrap(AppConfig config) async { WidgetsFlutterBinding.ensureInitialized(); final api = TelemedicineApi(baseUrl: config.apiUrl); runApp(TelemedicineApp(api: api, config: config)); } ``` In Design A each `main_*.dart` shrinks to one line calling `bootstrap`; in Design B `main.dart` builds the config from the environment and calls the same function. Either way, the code that differs per environment is a few lines, which is what keeps the two builds from drifting apart. ## What a lead would standardise - **One source of truth** per build: either the flavor name decides everything (config derived from `appFlavor`) or the define file does, with the other checked against it. - **Scripted builds**: CI jobs and IDE launch configurations that always pass the matching pair, so nobody types the combination by hand. - **Startup validation** that fails fast on a missing or mismatched value rather than silently defaulting to `''`. - **No secrets in either design** — both compile values into the binary. Neither design is wrong. The judgement is about how many independent switches you allow per build, and how early a wrong combination is caught.

  • How can a single main.dart keep a fake backend out of the production binary?
    Guard every reference to the fake behind a `const bool.fromEnvironment('USE_FAKE_BACKEND')` check. Because the value is a compile-time constant, the compiler can drop the unused branch and anything reachable only from it. A non-const flag, or a reference outside the guard, keeps the fake in the build.
  • How do you stop developers and CI from combining the wrong flavor and config file?
    Remove the choice: wrap builds in scripts or CI jobs that pass a fixed pair, commit IDE launch configurations per flavor, and add a startup check that compares `appFlavor` with an `ENVIRONMENT` define and throws on a mismatch.

saying these in an interview costs you the question

  • Thinks -t or --target changes the application ID or native project
  • Believes separate entry points remove the need for native flavors
  • Relies on developers typing the right flavor and file every time
  • Lets a missing define silently default to an empty API URL
  • Assumes a non-const feature flag removes mock code from release builds