Flutter Flavors dev, staging and prod: solved exercise
Flutter Flavors dev, staging and prod: solved exercise
No real production app has a single environment. Flavors (or build variants) let you run the same codebase with different configurations without a single if in your business logic. The most portable approach in Flutter is using --dart-define or --dart-define-from-file.
Problem statement
Configure three environments — dev, staging, and prod — with:
- A different API URL per environment.
- A differentiated app name (visible in the device launcher).
- Logging enabled only in
dev. - A screen that displays the active configuration.
Recommended approach: dart-define-from-file
Create one JSON file per environment (do not version files containing secrets):
Add envs/ to .gitignore if the files contain secrets (or inject them as CI environment variables).
Solution: AppConfig
Solution: Conditional logger
Solution: main.dart and info screen
Running each environment
VS Code integration (launch.json)
Expected result
flutter run --dart-define-from-file=envs/dev.json→ orange screen with “DEV” and dev URL.flutter run --dart-define-from-file=envs/prod.json→ green screen with “PROD”, logging disabled.- Without the flag → falls back to dev values (safe default).
Common mistakes
- Expecting hot reload to pick up dart-define changes: these values are resolved at compile time; changing the JSON file requires a full recompile, not just hot reload.
- Versioning files with real secrets: add the
.jsonenv files to.gitignore; in CI inject them as runner secrets or environment variables. - Changing the app name only in Dart: the name visible in the device launcher must be set in
AndroidManifest.xml(Android) andInfo.plist(iOS). Useflutter_launcher_nameor configure them manually.
Alternative: native flavors (Android/iOS)
For deeper changes (different app icons, separate Xcode targets), you need native flavors in Gradle and Xcode. --dart-define-from-file covers 80% of use cases with far less configuration.
Practical use
This is the first thing you configure in a new production project, alongside the base architecture. It prevents production API calls from development builds and makes QA much easier.
Recommended next exercise
- Flutter layered architecture: solved exercise
- Dependency injection with get_it in Flutter: solved exercise
- Dio with interceptors in Flutter: solved exercise
- All Flutter exercises
Guided practice and next step
- More Flutter exercises
- C exercises to strengthen fundamentals
- Programming in C in 100 Solved Exercises
- View the book on Amazon (included in Kindle Unlimited)
- Subscribe to the newsletter
FAQ
Does dart-define completely replace native flavors?
For most apps, yes. Native flavors are only necessary when you need separate app icons, distinct bundle IDs, or deep per-environment native configurations on Android/iOS.
Are dart-define values safe for API secrets?
Not completely. They are embedded in the binary and extractable with analysis tools. For sensitive secrets, use a backend intermediary or services like Firebase Remote Config.
Does it work with GitHub Actions?
Yes. Pass values as environment variables in the workflow and generate the environment JSON dynamically before the build: echo '{"API_BASE_URL":"${{ secrets.API_URL }}"}' > envs/prod.json.