Flutter Native Flavors: Android, iOS & Firebase Setup
In the previous article, we solved a developer workflow problem.
Instead of repeatedly typing commands like:
flutter run --dart-define=ENV=qa
we created launch configurations that allowed us to switch environments with a single click.
For many projects, that setup is already enough.
But eventually, teams run into requirements that environment variables alone cannot solve.
You may need:
Different application IDs
Different app icons
Separate Firebase projects
Separate push notification configurations
Separate Android and iOS signing setups
Multiple versions of the same app installed simultaneously
This is where native flavors enter the picture.
By the end of this article, you'll understand when Flutter flavors are actually needed, how Android and iOS implement them differently, how Firebase fits into the picture, and what a production-ready setup looks like.
Let's start with an important distinction.
dart-define vs Native Flavors
A lot of Flutter developers initially assume these two approaches compete with each other.
They don't.
They solve different problems.
--dart-define controls application behavior.
Examples:
API endpoints
Feature flags
Analytics settings
Logging configuration
Environment names
Native flavors control platform-specific configuration.
Examples:
Package names
App names
App icons
Firebase projects
Signing configurations
Think of it this way:
dart-define changes how your app behaves.
Native flavors change what app gets built.
In mature projects, both are usually used together.
When Native Flavors Become Necessary
Suppose you have a QA environment and a Production environment.
Using only dart-define, both builds still install as the same application.
That creates problems.
Every time a tester installs the QA build, the production version may be replaced.
The same issue appears when different Firebase projects, signing certificates, or notification configurations are required.
Native flavors solve this by creating distinct applications.
For example:
| Environment | Application ID |
|---|---|
| QA | com.company.app.qa |
| Production | com.company.app |
Now both applications can exist on the same device simultaneously.
That alone is often enough to justify flavors.
Android Product Flavors
Android provides flavor support through Gradle.
Inside android/app/build.gradle, you can define multiple product flavors.
android {
flavorDimensions "environment"
productFlavors {
qa {
dimension "environment"
applicationIdSuffix ".qa"
versionNameSuffix "-qa"
}
prod {
dimension "environment"
}
}
}
This creates two independent Android applications:
com.company.app.qa
com.company.app
Each flavor can also have:
Different icons
Different app names
Different resources
Different Firebase configurations
Building becomes straightforward.
flutter run --flavor qa
or
flutter run --flavor prod
At this point, Android can already distinguish between environments at the operating system level.
iOS Uses Schemes Instead of Flavors
iOS approaches the same problem differently.
Instead of product flavors, Xcode uses Schemes and Build Configurations.
A common setup looks like this:
QA
Production
Each scheme can point to different:
Bundle identifiers
Firebase configurations
Signing certificates
Build settings
For example:
com.company.app.qa
com.company.app
Conceptually, this achieves the same outcome as Android product flavors.
The terminology is different, but the goal is identical:
Build multiple versions of the same application from a single codebase.
The Real-World Setup: Flavor + dart-define
The most scalable Flutter projects combine both approaches.
Instead of choosing one or the other, they use each for what it does best.
Example:
flutter run \
--flavor qa \
--dart-define=ENV=qa
And:
flutter run \
--flavor prod \
--dart-define=ENV=prod
The flavor controls:
Application ID
Firebase project
Native resources
The environment variable controls:
API URLs
Feature toggles
Runtime configuration
This separation keeps responsibilities clear.
Native platforms handle native concerns.
Flutter handles application behavior.
Firebase and Multiple Environments
Firebase is often the reason teams adopt native flavors.
A typical project needs separate Firebase projects for:
Development
QA
Production
Why?
Because mixing environments creates operational headaches.
Imagine QA testers generating analytics data inside your production Firebase project.
Or test push notifications reaching real users.
Or development builds polluting production Crashlytics reports.
Separate Firebase projects eliminate these problems.
A common setup looks like this:
company-app-dev
company-app-qa
company-app-prod
Each environment receives its own:
Analytics
Crashlytics
Authentication
Firestore
Storage
Cloud Messaging
Now every environment is isolated.
That's usually what enterprise teams want.
Firebase Configuration Files
Each Firebase project generates platform-specific configuration files.
Android:
google-services.json
iOS:
GoogleService-Info.plist
The flavor determines which configuration file gets bundled into the application.
For example:
android/app/src/qa/google-services.json
android/app/src/prod/google-services.json
And similarly on iOS:
QA/GoogleService-Info.plist
Production/GoogleService-Info.plist
When the QA flavor is built, the QA Firebase project is used automatically.
When Production is built, the production Firebase project is used automatically.
No code changes are required.
What About Flutter Web?
Flutter Web introduces an interesting limitation.
There is no equivalent of Android application IDs or iOS bundle identifiers.
Everything ultimately runs inside the browser.
Because of this, web deployments usually rely heavily on:
--dart-define
instead of native flavors.
For example:
flutter build web \
--dart-define=ENV=qa
and
flutter build web \
--dart-define=ENV=prod
The deployment pipeline then decides where the generated build is hosted.
For most web projects, environment variables are sufficient.
Native flavor concepts matter far less than they do on Android and iOS.
What I Recommend for Most Teams
After working through environment configuration, launch configurations, and native flavors, a pattern emerges.
Small projects:
dart-define only
Growing projects:
launch configurations
+
dart-define
Production applications:
native flavors
+
dart-define
+
separate Firebase projects
This combination scales remarkably well.
It provides:
Clean developer workflows
Multiple installable app variants
Environment isolation
Safer releases
Better operational visibility
Most importantly, responsibilities remain clear.
Flutter manages runtime behavior.
Native flavors manage platform configuration.
Firebase remains isolated per environment.
Final Thoughts
This series started with a simple question:
"How should Flutter applications handle multiple environments?"
Along the way we covered:
Environment configuration fundamentals
Using dart-define effectively
Launch configurations for better developer workflow
Native flavors and Firebase environments
Notice how the solution evolved.
We didn't start with native flavors.
We started with the simplest approach that solved the problem.
Only when additional requirements appeared did we introduce more complexity.
That's usually the right engineering decision.
Start simple.
Add complexity only when it creates real value.
For many projects, dart-define is enough.
For production systems with multiple teams, Firebase environments, and release pipelines, native flavors become the missing piece that completes the architecture.
And that's where Flutter environment management stops being a development convenience and becomes a deployment strategy.