In Flutter development, it’s common to have different environments like development, staging, and production. When your Flutter app integrates with Firebase, it’s essential to set up separate Firebase projects (or backend environments) for each flavor to prevent data contamination and ensure a clean testing process. This guide provides a comprehensive approach to configuring separate Firebase projects for different flavors in Flutter.
Why Use Separate Firebase Projects/Environments?
- Data Isolation: Keeps test and production data separate.
- Clean Testing: Allows testing new features without affecting the live environment.
- Configuration Management: Eases managing different API keys and configurations for each environment.
Prerequisites
- Flutter environment set up.
- Firebase account with billing enabled.
- FlutterFire CLI installed and configured.
Step-by-Step Guide
Step 1: Create Firebase Projects
Create separate Firebase projects for each flavor in the Firebase Console:
- Go to the Firebase Console.
- Click “Add project.”
- Enter a project name for each environment (e.g., “MyApp-Dev,” “MyApp-Staging,” “MyApp-Prod”).
- Follow the prompts to create each project.
Step 2: Configure Flutter Flavors
Define different flavors in your Flutter app using flutter_launcher_icons.yaml
or command-line arguments.
Option 1: Using flutter_launcher_icons.yaml
First, add the flutter_launcher_icons
package to your dev_dependencies
in pubspec.yaml
:
dev_dependencies:
flutter_launcher_icons: ^0.13.1
Create or modify your flutter_launcher_icons.yaml
file to include different flavors:
flutter_launcher_icons:
android: true
ios: true
remove_alpha_ios: true
image_path: "assets/icon/icon.png"
flavor_dev:
android: true
ios: true
image_path: "assets/icon/icon-dev.png"
flavor_staging:
android: true
ios: true
image_path: "assets/icon/icon-staging.png"
Option 2: Using Command-Line Arguments
Alternatively, you can define flavors directly in your build commands:
flutter build apk --flavor dev -t lib/main_dev.dart
flutter build apk --flavor staging -t lib/main_staging.dart
flutter build apk --flavor prod -t lib/main_prod.dart
Step 3: Set Up FlutterFire CLI
Ensure FlutterFire CLI is installed and configured. If not, run:
dart pub global activate flutterfire_cli
Then, configure FlutterFire:
flutterfire configure
This command lets you select the Firebase project for your default configuration (e.g., production).
Step 4: Create Environment-Specific Firebase Options
For each flavor, you need to initialize Firebase with the appropriate options. Create separate files for each flavor to hold these options.
Create firebase_options_dev.dart
:
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get android {
return FirebaseOptions(
apiKey: "YOUR_DEV_API_KEY",
appId: "YOUR_DEV_APP_ID",
messagingSenderId: "YOUR_DEV_MESSAGING_SENDER_ID",
projectId: "YOUR_DEV_PROJECT_ID",
authDomain: "YOUR_DEV_AUTH_DOMAIN",
storageBucket: "YOUR_DEV_STORAGE_BUCKET",
);
}
static FirebaseOptions get ios {
return FirebaseOptions(
apiKey: "YOUR_DEV_API_KEY",
appId: "YOUR_DEV_APP_ID",
messagingSenderId: "YOUR_DEV_MESSAGING_SENDER_ID",
projectId: "YOUR_DEV_PROJECT_ID",
authDomain: "YOUR_DEV_AUTH_DOMAIN",
storageBucket: "YOUR_DEV_STORAGE_BUCKET",
iosClientId: "YOUR_DEV_IOS_CLIENT_ID",
iosBundleId: "YOUR_DEV_IOS_BUNDLE_ID",
);
}
static FirebaseOptions get currentPlatform {
if (defaultTargetPlatform == TargetPlatform.android) {
return android;
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return ios;
}
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
Replace YOUR_DEV_*
placeholders with actual values from your Firebase Dev project settings.
Create firebase_options_staging.dart
:
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get android {
return FirebaseOptions(
apiKey: "YOUR_STAGING_API_KEY",
appId: "YOUR_STAGING_APP_ID",
messagingSenderId: "YOUR_STAGING_MESSAGING_SENDER_ID",
projectId: "YOUR_STAGING_PROJECT_ID",
authDomain: "YOUR_STAGING_AUTH_DOMAIN",
storageBucket: "YOUR_STAGING_STORAGE_BUCKET",
);
}
static FirebaseOptions get ios {
return FirebaseOptions(
apiKey: "YOUR_STAGING_API_KEY",
appId: "YOUR_STAGING_APP_ID",
messagingSenderId: "YOUR_STAGING_MESSAGING_SENDER_ID",
projectId: "YOUR_STAGING_PROJECT_ID",
authDomain: "YOUR_STAGING_AUTH_DOMAIN",
storageBucket: "YOUR_STAGING_STORAGE_BUCKET",
iosClientId: "YOUR_STAGING_IOS_CLIENT_ID",
iosBundleId: "YOUR_STAGING_IOS_BUNDLE_ID",
);
}
static FirebaseOptions get currentPlatform {
if (defaultTargetPlatform == TargetPlatform.android) {
return android;
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return ios;
}
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
Replace YOUR_STAGING_*
placeholders with actual values from your Firebase Staging project settings.
(Optional) Create firebase_options_prod.dart
:
If you want to keep it separate from the FlutterFire CLI auto-generated file.
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get android {
return FirebaseOptions(
apiKey: "YOUR_PROD_API_KEY",
appId: "YOUR_PROD_APP_ID",
messagingSenderId: "YOUR_PROD_MESSAGING_SENDER_ID",
projectId: "YOUR_PROD_PROJECT_ID",
authDomain: "YOUR_PROD_AUTH_DOMAIN",
storageBucket: "YOUR_PROD_STORAGE_BUCKET",
);
}
static FirebaseOptions get ios {
return FirebaseOptions(
apiKey: "YOUR_PROD_API_KEY",
appId: "YOUR_PROD_APP_ID",
messagingSenderId: "YOUR_PROD_MESSAGING_SENDER_ID",
projectId: "YOUR_PROD_PROJECT_ID",
authDomain: "YOUR_PROD_AUTH_DOMAIN",
storageBucket: "YOUR_PROD_STORAGE_BUCKET",
iosClientId: "YOUR_PROD_IOS_CLIENT_ID",
iosBundleId: "YOUR_PROD_IOS_BUNDLE_ID",
);
}
static FirebaseOptions get currentPlatform {
if (defaultTargetPlatform == TargetPlatform.android) {
return android;
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return ios;
}
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
Replace YOUR_PROD_*
placeholders with actual values from your Firebase Prod project settings.
Step 5: Conditional Firebase Initialization
In your main.dart
files (or environment-specific main_dev.dart
, main_staging.dart
), conditionally initialize Firebase based on the flavor.
main_dev.dart
:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options_dev.dart' as dev_firebase_options;
import 'my_app.dart'; // Assuming you have a MyApp widget
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: dev_firebase_options.DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
main_staging.dart
:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options_staging.dart' as staging_firebase_options;
import 'my_app.dart'; // Assuming you have a MyApp widget
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: staging_firebase_options.DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
main.dart
(Production):
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; // This is auto-generated by FlutterFire CLI
import 'my_app.dart'; // Assuming you have a MyApp widget
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
Step 6: Run the App with Different Flavors
Run your Flutter app with the desired flavor using the following commands:
flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor staging -t lib/main_staging.dart
flutter run # defaults to production if no flavor specified and main.dart is production
Additional Tips
- Use Environment Variables: For sensitive data like API keys, consider using environment variables or Flutter’s
dotenv
package. - Automate Builds: Set up CI/CD pipelines (e.g., using GitHub Actions, GitLab CI) to automate building and deploying your app for each flavor.
- Firebase Environment Configuration: Consider extending this approach to other Firebase services like Remote Config for more fine-grained control.
Troubleshooting
- Firebase Initialization Errors: Ensure all Firebase options (API key, app ID, etc.) are correctly configured for each environment.
- Build Errors: Check that your
build.gradle
file is correctly set up for flavors, and dependencies are correctly added. - Runtime Errors: Double-check that you are using the correct
main.dart
file and that it matches your flavor setup.
Conclusion
Configuring separate Firebase projects (or backend environments) for each flavor in Flutter is essential for maintaining data integrity and enabling safe testing practices. By following this step-by-step guide, you can ensure that your development, staging, and production environments are isolated and properly configured, leading to a more stable and reliable app.