If your Flutter app supports multiple flavors and connects to Firebase, you need some extra setup to ensure each flavor corresponds to a different Firebase environment.
The best approach is to create a separate Firebase project for each flavor. This keeps your development, staging, and production environments separate.
When using custom backends or Dart SDKs like Supabase, you can connect to the correct environment by switching the URL and API key based on the flavor. But Firebase does not offer a Dart SDK and requires some platform-specific setup, making the flavoring process more complex.
Thankfully, the FlutterFire CLI comes to the rescue. I'll walk you through using it to flavor your Flutter & Firebase apps without losing your mind. đ
Here's what we will cover:
- Why do we need FlutterFire?
- Installing the Firebase and FlutterFire CLI
- FlutterFire Config Syntax for Multiple Flavors
- Easier Setup with a Shell Script
- Initializing Firebase during App Startup (iOS, Android, and web)
By the end, you'll be able to confidently integrate Firebase into your multi-flavor Flutter app, saving time and avoiding common setup headaches.
Prerequisites
Note: This guide assumes you already have a Flutter app that runs correctly with dev
, stg
, and prod
flavors on iOS and Android:
flutter run --flavor dev
flutter run --flavor stg
flutter run --flavor prod
You'll also need three Firebase projects, like in this example:
You can use Flutter Flavorizr to add flavors to your app. For more guidance, check out my Flutter in Production course.
Ready? Let's go! đ
Why do we need FlutterFire?
Adding Firebase to a Flutter app used to be a tedious process. Youâd have to manually download configuration files for each platform (like GoogleService-Info.plist
 for iOS and google-services.json
 for Android).
Now, the process is much simpler. You just run flutterfire configure
 and follow some interactive prompts. Once finished, these files are automatically added to your project:
lib/firebase_options.dart
ios/Runner/GoogleService-Info.plist
android/app/google-services.json
However, when working with multiple flavors, it gets trickier. Youâll need separate versions of these files for each flavor, stored in different locations to avoid overwriting them during the configuration process.
Luckily, FlutterFire 1.0.0 added support for multiple flavors. Let's explore how to use it.
Installing the Firebase and FlutterFire CLI
The official docs cover all the steps for installing the Firebase and FlutterFire CLIs.
The Firebase CLI can be installed as a standalone library or via npm. Iâve found the npm
approach to be the most reliable:
npm install -g firebase-tools
To check your installation, run: firebase --version
.
Next, log in by running firebase login
. Youâll see this prompt:
? Allow Firebase to collect CLI and Emulator Suite usage and error reporting information? Yes
This opens a browser window where you can sign in with Google. After selecting your account, youâll see this:
Click "Allow" and close the window. Now, the Firebase CLI is logged in.
Note: Make sure you use the Google account linked to the Firebase projects you want to work with. If youâre logged in with the wrong account, runÂ
firebase logout
 andfirebase login
again.
Installing the FlutterFire CLI
To install the FlutterFire CLI, run:
dart pub global activate flutterfire_cli
Then, check that youâre on version 1.0.0
 or above by running flutterfire --version
.
FlutterFire Config Syntax with Multiple Flavors
Let's consider this command, which generates all the config files for the dev
 flavor:
flutterfire config \
--project=flutter-ship-dev \
--out=lib/firebase_options_dev.dart \
--ios-bundle-id=com.codewithandrea.flutterShipApp.dev \
--ios-out=ios/flavors/dev/GoogleService-Info.plist \
--android-package-name=com.codewithandrea.flutter_ship_app.dev \
--android-out=android/app/src/dev/google-services.json
Hereâs what each argument does:
--project
: The Firebase project to use (note: pass the project ID, not the alias).--out
: Output path for the Firebase config file.--ios-bundle-id
: iOS appâs bundle ID. Find it in Xcode under Runner > General > Identity > Bundle Identifier.--ios-out
: Output path for the iOSÂGoogleService-Info.plist
.--android-package-name
: Android appâs package name (found asÂapplicationId
 inÂandroid/app/build.gradle
).--android-out
: Output path for the AndroidÂgoogle-services.json
.
To learn about all the available options, run
flutterfire config --help
.
To use this command, you can:
- Copy it into your terminal.
- Update theÂ
project
,Âios-bundle-id
, andÂandroid-package-name
 for your app. - Run it and follow the interactive prompts (weâll cover these in a moment).
But youâll need to repeat this for the stg
 and prod
 flavors. Thatâs time-consuming and error prone.
Let's see if we can automate the process. đ
Easier Setup with a Shell Script
While flutterfire config
 handles most of the work, you still need to run it for each flavor with different arguments.
To streamline this, create a flutterfire-config.sh
 script and save it at the root of your project:
#!/bin/bash
# Script to generate Firebase configuration files for different environments/flavors
# Feel free to reuse and adapt this script for your own projects
if [[ $# -eq 0 ]]; then
echo "Error: No environment specified. Use 'dev', 'stg', or 'prod'."
exit 1
fi
case $1 in
dev)
flutterfire config \
--project=flutter-ship-dev \
--out=lib/firebase_options_dev.dart \
--ios-bundle-id=com.codewithandrea.flutterShipApp.dev \
--ios-out=ios/flavors/dev/GoogleService-Info.plist \
--android-package-name=com.codewithandrea.flutter_ship_app.dev \
--android-out=android/app/src/dev/google-services.json
;;
stg)
flutterfire config \
--project=flutter-ship-stg \
--out=lib/firebase_options_stg.dart \
--ios-bundle-id=com.codewithandrea.flutterShipApp.stg \
--ios-out=ios/flavors/stg/GoogleService-Info.plist \
--android-package-name=com.codewithandrea.flutter_ship_app.stg \
--android-out=android/app/src/stg/google-services.json
;;
prod)
flutterfire config \
--project=flutter-ship-prod \
--out=lib/firebase_options_prod.dart \
--ios-bundle-id=com.codewithandrea.flutterShipApp \
--ios-out=ios/flavors/prod/GoogleService-Info.plist \
--android-package-name=com.codewithandrea.flutter_ship_app \
--android-out=android/app/src/prod/google-services.json
;;
*)
echo "Error: Invalid environment specified. Use 'dev', 'stg', or 'prod'."
exit 1
;;
esac
With this script, you still need to set the correct arguments for your project, but you only need to do this once.
Then, generating all the Firebase config files becomes a breezeâno need to remember each argument.
Time to take take the script for a ride. đ
Running the FlutterFire Script for each Flavor
To configure the dev
flavor, run:
./flutterfire-config.sh dev
When prompted, select "Build configuration":
? You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. âș
⯠Build configuration
Target
Then, choose the Debug-dev
 build configuration:
? Please choose one of the following build configurations âș
Debug
Release
Profile
⯠Debug-dev
Profile-dev
Release-dev
Debug-stg
Profile-stg
Release-stg
Debug-prod
Profile-prod
Release-prod
Note: If you encounter a "Failed to list Firebase projects" error, runÂ
firebase logout
, thenÂfirebase login
, and try again.
Next, choose the platforms you want to configure:
? Which platforms should your configuration support (use arrow keys & space to select)? âș
â android
â ios
macos
â web
windows
This step may take some time as the CLI registers the necessary apps with Firebase. If successful, youâll see a confirmation similar to this:
â You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. · Build configuration
â Please choose one of the following build configurations · Debug-dev
i Found 40 Firebase projects. Selecting project flutter-ship-dev.
â Which platforms should your configuration support (use arrow keys & space to select)? · android, ios, web
i Firebase android app com.codewithandrea.flutter_ship_app.dev is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase android app on Firebase project flutter-ship-dev.
i Firebase ios app com.codewithandrea.flutterShipApp.dev is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase ios app on Firebase project flutter-ship-dev.
i Firebase web app flutter_ship_app (web) is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase web app on Firebase project flutter-ship-dev.
Firebase configuration file lib/firebase_options_dev.dart generated successfully with the following Firebase apps:
Platform Firebase App Id
web 1:424176442589:web:c86e231d1eeaba0e90cf34
android 1:424176442589:android:c5841ba53606b4c490cf34
ios 1:424176442589:ios:592b56a800affa4e90cf34
Learn more about using this file and next steps from the documentation:
> https://firebase.google.com/docs/flutter/setup
Next, repeat the same steps for the stg
 flavor by running:
./flutterfire-config.sh stg
And again for the prod
 flavor:
./flutterfire-config.sh prod
Once complete, your project will have these new files:
lib/firebase_options_dev.dart
lib/firebase_options_stg.dart
lib/firebase_options_prod.dart
ios/flavors/dev/GoogleService-Info.plist
ios/flavors/stg/GoogleService-Info.plist
ios/flavors/prod/GoogleService-Info.plist
android/app/src/dev/google-services.json
android/app/src/stg/google-services.json
android/app/src/prod/google-services.json
Should the Firebase config files be added to Git?
The files above donât contain sensitive information, so itâs safe to commit them to Git.
However, in my open-source projects, I prefer to add them to .gitignore
:
# Ignore Firebase configuration files
lib/firebase_options*.dart
ios/Runner/GoogleService-Info.plist
ios/flavors/*/GoogleService-Info.plist
macos/Runner/GoogleService-Info.plist
macos/flavors/*/GoogleService-Info.plist
android/app/google-services.json
android/app/src/*/google-services.json
This has two implications:
- For a fresh checkout, youâll need to runÂ
flutterfire-config.sh
 again for each flavor. - On CI, you can store these files as environment secrets and add a pre-build step to restore them to their correct locations.
FlutterFire setup complete â
If you followed all the steps above without errors, all the Firebase configuration files should now be in your project.
Before running the app with Firebase, there are a few more steps to tackle:
- Install the
firebase_core
package and verify the app runs on Android and iOS - Initialize Firebase when the app starts
Letâs walk through them. đ
Installing the firebase_core package
To add firebase_core
, run this in your terminal:
flutter pub add firebase_core
flutter pub get
Running the app on Android
If you try to run the Android app, you may see an error about the "com.google.gms.google-services" plugin not being found.
To fix this, open android/build.gradle
 and add a buildscript
 section at the top:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.3.0'
classpath 'com.google.gms:google-services:4.4.2'
}
}
How to Choose the Correct Versions?
- Find the Gradle version inÂ
gradle-wrapper.properties
. - Check the latest version ofÂ
google-services
 in Google's Maven Repository.
After applying this fix, the Android app should run correctly.
Running the app on iOS
Before running the iOS app, open ios/Podfile
 and ensure the platform version is set to 13.0
 or higher:
# Uncomment this line to define a global platform for your project
platform :ios, '13.0'
Run pod install
, and you should be able to run the app on iOS.
Firebase Initialization During App Startup
According to the official docs, you should add the Firebase initialization code to lib/main.dart
:
import 'package:flutter_ship_app/firebase_options.dart';
// inside main()
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
However, this default setup wonât work for us because we have separate configuration files for each flavor:
So, how can we handle this?
Option 1: Centralize the Firebase Initialization logic
One option is to create a firebase.dart
 file with the following code:
// firebase.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ship_app/firebase_options_prod.dart' as prod;
import 'package:flutter_ship_app/firebase_options_stg.dart' as stg;
import 'package:flutter_ship_app/firebase_options_dev.dart' as dev;
Future<void> initializeFirebaseApp() async {
// Determine which Firebase options to use based on the flavor
final firebaseOptions = switch (appFlavor) {
'prod' => prod.DefaultFirebaseOptions.currentPlatform,
'stg' => stg.DefaultFirebaseOptions.currentPlatform,
'dev' => dev.DefaultFirebaseOptions.currentPlatform,
_ => throw UnsupportedError('Invalid flavor: $flavor'),
};
await Firebase.initializeApp(options: firebaseOptions);
}
This works by switching on the appFlavor
 constant to return the correct FirebaseOptions
 object based on the flavor.
Note: When running on Flutter web with the
--flavor
option, you'll get a warning you that flavors are not fully supported. But theappFlavor
constant will still return the correct value.
Now, you can simply call await initializeFirebaseApp()
 in lib/main.dart
, which remains the single entry point for the app:
import 'firebase.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebaseApp();
runApp(const MainApp());
}
By default, Flutter Flavorizr generates separate entry points likeÂ
main_dev.dart
,Âmain_stg.dart
, andÂmain_prod.dart
. This can lead to unwanted code duplication, so I prefer using a singleÂmain.dart
, as shown above, with switch expressions to handle flavor-specific initialization (there's a caveat thoughâmore on this below).
With this setup, the Flutter app will initialize and connect to the correct Firebase project, depending on the flavor.
However, there's one issue. đ
All the Firebase Config Files are Bundled (No Tree Shaking)
If you look closely at firebase.dart
, youâll notice that although the correct Firebase config is selected based on the flavor, all three firebase_options_*.dart
 files are still imported:
// firebase.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
// Note: all three files are imported
import 'package:flutter_ship_app/firebase_options_prod.dart' as prod;
import 'package:flutter_ship_app/firebase_options_stg.dart' as stg;
import 'package:flutter_ship_app/firebase_options_dev.dart' as dev;
Future<void> initializeFirebaseApp() async {
// Determine which Firebase options to use based on the flavor
final firebaseOptions = switch (appFlavor) {
'prod' => prod.DefaultFirebaseOptions.currentPlatform,
'stg' => stg.DefaultFirebaseOptions.currentPlatform,
'dev' => dev.DefaultFirebaseOptions.currentPlatform,
_ => throw UnsupportedError('Invalid flavor: $flavor'),
};
await Firebase.initializeApp(options: firebaseOptions);
}
This means that all three files are compiled and bundled during the build process because tree-shaking doesn't work here (the switch
happens at runtime).
In theory, this could expose your development or staging environment details (which may be less secure than production) if someone reverse engineers your app.
While this might not be a big issue for apps that donât handle sensitive data, itâs still a potential risk. If you want to mitigate this entirely, consider a more secure approach. đ
Option 2: Use Multiple Entry Points
As we've seen, this code can be problematic:
import 'package:flutter_ship_app/firebase_options_prod.dart' as prod;
import 'package:flutter_ship_app/firebase_options_stg.dart' as stg;
import 'package:flutter_ship_app/firebase_options_dev.dart' as dev;
A more secure approach is to create three separate entry pointsâmain_dev.dart
, main_stg.dart
, and main_prod.dart
âwhich look like this:
// main_dev.dart
import 'package:flutter_ship_app/firebase_options_dev.dart';
import 'main.dart';
void main() async {
runMainApp(DefaultFirebaseOptions.currentPlatform);
}
These files should do one thing only: import the correct firebase_options_*.dart
file and pass the config as an argument to a function inside main.dart
that performs the actual initialization. Hereâs an example:
// main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
void runMainApp(FirebaseOptions firebaseOptions) async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: firebaseOptions);
runApp(const MainApp());
}
This approach ensures that only the required Firebase configuration file is bundled, making it a secure and efficient solution for managing multiple flavors.
But how do you run the app with the right flavor? đ
Running the App with a Specific Flavor
If you use the second option outlined above, you'll have four files:
main_dev.dart
: Entry point fordev
main_stg.dart
: Entry point forstg
main_prod.dart
: Entry point forprod
main.dart
: Contains the app initialization code
As a result, you can run the app with a specific flavor using these commands:
flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor stg -t lib/main_stg.dart
flutter run --flavor prod -t lib/main_prod.dart
This way, the correct entry point is used for each flavor, ensuring that the right Firebase environment is connected when the app launches.
If you follow this approach, make sure to update your local configuration (e.g.,
.vscode/launch.json
) and CI/CD scripts to reflect these changes.
Which Option Should you Choose?
Both options have their pros and cons, and the right choice depends on your projectâs needs:
- Option 1: Centralized Firebase Initialization. This approach is easier and quicker to implement. It allows you to use a single
main.dart
file and handle flavor-specific Firebase options dynamically at runtime. However, because all Firebase configuration files are bundled in the final app (even if theyâre not used), it's not the best choice for security reasons. - Option 2: Multiple Entry Points for Each Flavor. This option requires a bit more setup because youâll need to create separate entry points for each flavor (
main_dev.dart
,main_stg.dart
,main_prod.dart
). However, it only bundles the necessary Firebase configuration file for each build, making it a more secure solution, since an attacker wonât have access to the environment details of other flavors.
While option 2 takes a bit more work, I recommend it for multi-flavor Flutter apps that use Firebase. đ
Option 1 works just fine for non-Firebase apps, since you can use
--dart-define-from-file
and define environment variables inside separate files (e.g..env.dev
,.env.stg
,.env.prod
) for each flavor. To learn more, read: How to Store API Keys in Flutter: --dart-define vs .env files.
Conclusion
By leveraging FlutterFire alongside a simple shell script, weâve streamlined what used to be a complex and error-prone flavoring process. Instead of manually configuring Firebase for each flavor, you can now generate the necessary files for all environments with a single script, saving time and reducing the chance of mistakes.
The centralized Firebase initialization option offers a quick and simple way to connect your app to the correct Firebase project at startup, using a single main.dart
file and flavor-specific logic. However, this approach bundles all Firebase configurations, which may not be ideal for security-sensitive apps.
For more secure setups, the multiple entry points strategy ensures that only the necessary Firebase configuration is included in each build, making it a better choice when handling sensitive data or production-grade apps.
With either approach, your Flutter app will automatically connect to the appropriate Firebase environmentâwhether thatâs dev
, stg
, or prod
âensuring your app behaves as expected in every stage of development and production.
This setup makes it easier to manage multiple flavors in your Flutter & Firebase apps, and I've been happily using it in production for my own apps. â
New Course: Flutter in Production
When it comes to shipping and maintaining apps in production, there are many important aspects to consider:
- Preparing for release: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&Cs
- App Submissions: app store metadata & screenshots, compliance, testing vs distribution tracks, dealing with rejections
- Release automation: CI workflows, environment variables, custom build steps, code signing, uploading to the stores
- Post-release: error monitoring, bug fixes, addressing user feedback, adding new features, over-the-air updates
My latest course will help you get your app to the stores faster and with fewer headaches.
If youâre interested, you can learn more and enroll here (currently 40% off!). đ