Setting Up Separate Firebase Projects or Backend Environments for Each Flavor in Flutter

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:

  1. Go to the Firebase Console.
  2. Click “Add project.”
  3. Enter a project name for each environment (e.g., “MyApp-Dev,” “MyApp-Staging,” “MyApp-Prod”).
  4. 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.