Flutter Flavors: Because Your App Has Multiple Personalities
And that's totally okay.
Have you ever ordered a coffee and been asked, "Do you want it hot or iced?" Same coffee beans, same water, same barista — but the experience is different depending on what you pick. That's basically what Flutter Flavors are. Same app, different configurations.
Let me walk you through it without the usual "read-the-docs" headache.
The Problem Nobody Talks About (Until It Bites You)
Picture this. You're building an app. Everything works beautifully on your machine. You push it to your client for testing, and — oh no — the test build is hitting the production server. Real users are seeing test data. Your client is not happy. You are not happy. Nobody is happy.
This happens more often than anyone admits.
The truth is, most apps need to exist in more than one version at the same time. You need a development version that talks to your local server. A staging version that your QA team can break without consequences. And a production version that goes to real users who don't care about your internal drama.
Manually changing API URLs, app names, and icons every time you switch between these? That's not development. That's suffering.
This is the problem Flutter Flavors solve.
So What Exactly Are Flavors?
Think of flavors as outfits for your app. The body underneath is the same — your code, your widgets, your logic. But depending on the occasion, you dress it differently.
A flavor lets you create different versions of your app from the same codebase, where each version can have its own:
- App name (so you can tell them apart on your phone)
- App icon
- API base URL
- Color theme
- Firebase project
- Any configuration you can think of
The best part? You don't duplicate a single line of business logic.
Let's Build This Thing (Step by Step)
Enough theory. Let's get our hands dirty.
Step 1: Define Your Flavors
First, decide what flavors you need. For most projects, three is the sweet spot:
| Flavor | Purpose | Example App Name |
|---|---|---|
dev | Local development | MyApp (Dev) |
staging | QA and client testing | MyApp (Staging) |
prod | Real users, real data | MyApp |
Step 2: Create a Flavor Configuration Class
Create a simple Dart file that holds the configuration for each flavor. I like to put this in lib/config/app_config.dart.
enum Flavor { dev, staging, prod }
class AppConfig {
final Flavor flavor;
final String appName;
final String apiBaseUrl;
static late AppConfig _instance;
factory AppConfig({
required Flavor flavor,
required String appName,
required String apiBaseUrl,
}) {
_instance = AppConfig._internal(
flavor: flavor,
appName: appName,
apiBaseUrl: apiBaseUrl,
);
return _instance;
}
AppConfig._internal({
required this.flavor,
required this.appName,
required this.apiBaseUrl,
});
static AppConfig get instance => _instance;
static bool get isDev => _instance.flavor == Flavor.dev;
static bool get isStaging => _instance.flavor == Flavor.staging;
static bool get isProd => _instance.flavor == Flavor.prod;
}
Nothing fancy here. Just a class that says, "Hey, here's who I am and where I should be talking to."
Step 3: Create Entry Points for Each Flavor
Instead of one main.dart, you'll create a separate entry point for each flavor. Here's the key idea: each entry point configures the app differently, then runs the same app.
lib/main_dev.dart
import 'package:flutter/material.dart';
import 'config/app_config.dart';
import 'app.dart';
void main() {
AppConfig(
flavor: Flavor.dev,
appName: 'MyApp (Dev)',
apiBaseUrl: 'https://dev-api.myapp.com',
);
runApp(const MyApp());
}
lib/main_staging.dart
import 'package:flutter/material.dart';
import 'config/app_config.dart';
import 'app.dart';
void main() {
AppConfig(
flavor: Flavor.staging,
appName: 'MyApp (Staging)',
apiBaseUrl: 'https://staging-api.myapp.com',
);
runApp(const MyApp());
}
lib/main_prod.dart
import 'package:flutter/material.dart';
import 'config/app_config.dart';
import 'app.dart';
void main() {
AppConfig(
flavor: Flavor.prod,
appName: 'MyApp',
apiBaseUrl: 'https://api.myapp.com',
);
runApp(const MyApp());
}
See the pattern? Same app. Different outfit.
Step 4: Use the Config in Your App
Now anywhere in your app, you can access the current flavor's configuration:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: AppConfig.instance.appName,
home: const HomeScreen(),
);
}
}
And in your API service:
class ApiService {
final String baseUrl = AppConfig.instance.apiBaseUrl;
Future<Response> getUsers() {
return http.get(Uri.parse('$baseUrl/users'));
}
}
That's it. No if-else spaghetti. No environment variables you'll forget to change. The configuration is baked in at launch time.
Step 5: Run the Right Flavor
Here's the fun part. You just tell Flutter which entry point to use:
# Development
flutter run -t lib/main_dev.dart
# Staging
flutter run -t lib/main_staging.dart
# Production
flutter run -t lib/main_prod.dart
The -t flag stands for target. You're telling Flutter, "Start from this file instead of the default main.dart."
Adding a Visual Indicator (The Banner Trick)
Here's a small trick I love. When you're running the dev or staging version, show a little banner so you always know which version you're looking at. No more "wait, is this staging or prod?" moments.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: AppConfig.instance.appName,
home: AppConfig.isProd
? const HomeScreen()
: Banner(
message: AppConfig.instance.flavor.name.toUpperCase(),
location: BannerLocation.topStart,
color: AppConfig.isDev ? Colors.green : Colors.orange,
child: const HomeScreen(),
),
);
}
}
Now your dev build has a green "DEV" banner, staging has an orange "STAGING" banner, and production is clean with no banner at all. Simple. Effective. Saves lives (okay, maybe just saves confusion).
Going Deeper: Platform-Level Flavors
What we've built so far works great for Dart-level configuration. But what if you want each flavor to have a different app icon or a different app name on the home screen? That requires configuring flavors at the platform level too.
Android
In your android/app/build.gradle, add flavor dimensions:
android {
// ... existing config
flavorDimensions "app"
productFlavors {
dev {
dimension "app"
applicationIdSuffix ".dev"
resValue "string", "app_name", "MyApp (Dev)"
}
staging {
dimension "app"
applicationIdSuffix ".staging"
resValue "string", "app_name", "MyApp (Staging)"
}
prod {
dimension "app"
resValue "string", "app_name", "MyApp"
}
}
}
The applicationIdSuffix is the magic sauce here. It means com.mycompany.myapp.dev and com.mycompany.myapp.staging are treated as separate apps. You can install all three on the same phone at the same time. No more uninstalling and reinstalling.
Then update your AndroidManifest.xml to use the dynamic app name:
<application
android:label="@string/app_name"
...>
iOS
For iOS, you'll need to set up build configurations in Xcode:
- Open
ios/Runner.xcworkspacein Xcode - Go to Runner > Project > Info > Configurations
- Duplicate your existing configurations (Debug, Release, Profile) for each flavor
- Name them:
Debug-dev,Release-dev,Debug-staging,Release-staging,Debug-prod,Release-prod - Create matching schemes for each flavor
Then add a flavor-specific xcconfig file for each flavor. For example, ios/Flutter/dev.xcconfig:
PRODUCT_BUNDLE_IDENTIFIER=com.mycompany.myapp.dev
PRODUCT_NAME=MyApp (Dev)
I won't lie — iOS setup is more involved than Android. But you only do it once.
Running with Platform Flavors
Once platform flavors are configured, use the --flavor flag:
# Development
flutter run --flavor dev -t lib/main_dev.dart
# Staging
flutter run --flavor staging -t lib/main_staging.dart
# Production
flutter run --flavor prod -t lib/main_prod.dart
Pro Tips From Someone Who Learned the Hard Way
1. Create run configurations in your IDE.
If you use VS Code, add this to your .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Dev",
"request": "launch",
"type": "dart",
"program": "lib/main_dev.dart",
"args": ["--flavor", "dev"]
},
{
"name": "Staging",
"request": "launch",
"type": "dart",
"program": "lib/main_staging.dart",
"args": ["--flavor", "staging"]
},
{
"name": "Prod",
"request": "launch",
"type": "dart",
"program": "lib/main_prod.dart",
"args": ["--flavor", "prod"]
}
]
}
Now you just pick a flavor from the dropdown and hit run. No typing commands. No typos.
2. Add a flavor-aware debug banner. We already covered this, but seriously, do it. Future you will thank present you.
3. Never hardcode API URLs.
If you find yourself writing https://api.myapp.com directly in a service file, stop. That URL belongs in AppConfig. Always.
4. Use different Firebase projects per flavor.
Each flavor should point to its own Firebase project. Dev data and production data should never, ever mix. Use the firebase_options_dev.dart, firebase_options_staging.dart, and firebase_options_prod.dart pattern with FlutterFire CLI to generate separate configs.
5. Keep your main.dart file.
Some tools and packages expect a main.dart to exist. I usually keep it and just make it point to the dev flavor by default:
// lib/main.dart
export 'main_dev.dart';
When Should You Set Up Flavors?
Honestly? At the start of the project.
Setting up flavors takes 30 minutes at the beginning of a project. Retrofitting them into an existing project takes a full afternoon and a few cups of strong coffee. It's one of those things that's always easier to do early.
Even if you think your app is "too small" for flavors, set them up anyway. You'll have at least a dev and prod environment, and you'll avoid accidentally shipping test data to production. Trust me, it happens to the best of us.
Wrapping Up
Flutter Flavors aren't complicated. They're just a structured way to say: "Same app, different settings." The concept is simple, but the impact on your workflow is massive. No more environment mixups. No more "which server am I hitting?" confusion. No more installing and uninstalling builds on your test device.
Set it up once, and you'll wonder how you ever lived without it.