Skip to main content

Command Palette

Search for a command to run...

Flutter Native Flavors: Android, iOS & Firebase Setup

Updated
6 min read

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:

  1. Environment configuration fundamentals

  2. Using dart-define effectively

  3. Launch configurations for better developer workflow

  4. 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.

More from this blog