Flutter iOS — Setup Flavors with different Firebase Config

Yusuf Fachroni
7 min readJun 3, 2023

--

What is the purpose of flavors? Flavors serve as a means to customize your application for different contexts, including development and production. Let’s consider an illustration:

  • During the development phase, you may desire your app to establish a connection with the API host at https://dev.mobileapp.com/v1/ using the project ID com.mobileapp.dev.
  • Conversely, when it’s time to release the app, the production version should connect to https://api.mobileapp.com/v1/ using the project ID com.mobileapp.prod.

Instead of manually coding these values into variables and creating separate app builds for each environment, it is recommended to utilize flavors. By employing flavors, you can provide these values as build-time configurations, streamlining the process.

In this tutorial, we will follow a methodology that involves creating a sample application with two flavors: “dev” and “prod”. After each step, commits will be made to the sample app, enabling you to review code differences and grasp the modifications made.

Additionally, I will provide detailed instructions to ensure a user-friendly experience, making it simple to apply these instructions to an existing app without any hassle.

Step 1: Create a Flutter application project

Step 2: Configure the application to integrate with Firebase

To begin, establish two distinct Firebase projects — one for development and another for production. Within the Firebase console, create an Android application for each project. Let’s assume the development app has the ID com.mobileapp.dev, while the production app has the ID com.mobileapp.prod. Ensure that you download the respective GoogleServices-Info.plist files for these two applications. For detailed instructions on how to integrate Firebase into your Flutter project, refer to the Firebase Flutter Setup Guide.

Create 2 Firebase Project for Dev & Prod

Next, copy the GoogleServices-Info.plist files for the development and production applications into separate folders within the ios/config/ directory

Step 3: Create 2 configuration file in root project directory for set pointing to different api endpoint

dev.json

prod.json

Step 4: Create multiple Runner on XCode

Lets open ios folder on XCode, simply right click on ios folder and you can find Open in Xcode option

Xcode introduces the concept of schemes and build configurations as counterparts to product flavors in Android. Let’s create custom scheme for Dev and Prod

Then, let’s create 3 configurations named Debug-Dev, Release-Dev, and Profile-Dev

We also need to rename for prod build configuration

Ok, now let’s set Dev schemes by Dev build configuration

  • Set Bundle ID based on flavor

We now have two schemes linked to their respective build configurations. This allows us to tailor customizations for each scheme. To begin, let’s modify the app bundle identifier to be distinct for both schemes.

click Runner on Targets section > Build Settings > type Bundle on filter top-right field > fill all Product Bundle Identifier

  • Set AppName based on flavors

We would like to utilize distinct Display Names for the app. However, the Display Name parameter is not available in the Build Settings for the target. As a workaround, we can create a user-defined parameter and utilize it in place of the Display Name parameter.

click Build Settings > Info

set Bundle display name to $(APP_DISPLAY_NAME)

click Build Settings > Click add button (+) > Add User-Defined Setting and type APP_DISPLAY_NAME

and set like this

  • Add config folder to Runner

Finally, we need to find a solution to utilize a different GoogleServices-Info.plist file based on the build configuration. Some suggestions propose handling this at runtime during app startup by explicitly specifying the desired configuration file when initializing Firebase (as mentioned in the Firebase documentation: https://firebase.google.com/docs/projects/multiprojects). However, another option I prefer is to copy the appropriate file to the default location during build time. This way, when the app bundle is generated, it automatically uses the correct file.

To achieve this, we will start by organizing the GoogleServices-Info.plist files for each flavor in separate folders, and add to Runner

Result config folder inside Runner
  • Add runner script to copy GoogleService-info.plist on build phase

Next, we need to determine how to include a step in the build process to ensure that the corresponding GoogleServices-Info.plist file is copied to the appropriate location, specifically within the Runner directory. This can be achieved by adding a new Run script Build Phase to the target.

environment="default"

# Regex to extract the scheme name from the Build Configuration
# We have named our Build Configurations as Debug-dev, Debug-prod etc.
# Here, dev and prod are the scheme names. This kind of naming is required by Flutter for flavors to work.
# We are using the $CONFIGURATION variable available in the XCode build environment to extract
# the environment (or flavor)
# For eg.
# If CONFIGURATION="Debug-prod", then environment will get set to "prod".
if [[ $CONFIGURATION =~ -([^-]*)$ ]]; then
environment=${BASH_REMATCH[1]}
fi

echo $environment

# Name and path of the resource we're copying
GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist
GOOGLESERVICE_INFO_FILE=${PROJECT_DIR}/config/${environment}/${GOOGLESERVICE_INFO_PLIST}

# Make sure GoogleService-Info.plist exists
echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_FILE}"
if [ ! -f $GOOGLESERVICE_INFO_FILE ]
then
echo "No GoogleService-Info.plist found. Please ensure it's in the proper directory."
exit 1
fi

# Get a reference to the destination location for the GoogleService-Info.plist
# This is the default location where Firebase init code expects to find GoogleServices-Info.plist file
PLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app
echo "Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}"

# Copy over the prod GoogleService-Info.plist for Release builds
cp "${GOOGLESERVICE_INFO_FILE}" "${PLIST_DESTINATION}"

Step 5: Create some dart files to handle configuration based on flavor

lib/utils/environment.dart

abstract class Environment {
static const dev = 'dev';
static const prod = 'prod';
}

lib/utils/config_reader.dart

import 'dart:convert';
import 'package:flutter/services.dart';
abstract class ConfigReader {
static Map<String, dynamic>? _config;
static bool _isDevMode = false;
static Future<void> initialize(String env) async {
var configString = '{}';
try {
configString = await rootBundle.loadString('config/$env.json');
} catch (_) {
configString = await rootBundle.loadString('config/dev.json');
}
_config = json.decode(configString) as Map<String, dynamic>;
_isDevMode = env == "dev";
}
static bool isDevMode() {
return _isDevMode;
}
static String getBaseUrl() {
return _config!['baseUrl'] as String;
}
}

Step 6: Create 3 different main dart files to manage flavor

lib/main_dev.dart

Future<void> main() async {
await mainCommon(Environment.dev);
}

lib/main_prod.dart

Future<void> main() async {
await mainCommon(Environment.prod);
}

lib/main_common.dart

Future<void> mainCommon(String env) async {
WidgetsFlutterBinding.ensureInitialized();
await ConfigReader.initialize(env);
runApp(const MyApp());
}

Step 6: Get a variable based on flavor

You can get variables from config like this

debugShowCheckedModeBanner: ConfigReader.isDevMode(),

And get base_url for connection class file like this

class HttpGetConnect extends GetConnect {
final _baseUrl = ConfigReader.getBaseUrl();
static HttpGetConnect? _instance;
HttpGetConnect._internal() {
_instance = this;
httpClient.baseUrl = _baseUrl;
}
}

Step 7: Add vscode launcher config

create .vscode/lunch.json

change launch.json to this code

{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "[Debug] Development App",
"request": "launch",
"type": "dart",
"program" : "lib/main_dev.dart",
"args": [
"--flavor",
"Dev"
]
},
{
"name": "[Debug] Production App",
"request": "launch",
"type": "dart",
"program" : "lib/main_prod.dart",
"args": [
"--flavor",
"Prod"
]
},
{
"name": "[Release] Production App",
"request": "launch",
"type": "dart",
"flutterMode": "release",
"program" : "lib/main_prod.dart",
"args": [
"--flavor",
"Prod"
]
},
]
}

and now you can launch your app using dev flavor or prod flavor

and here is the result

Left: Running production in debug mode | Right: Running development in debug mode

Step 8: Build xcarchive

Open XCode, then select Target Runners > Signing & Capabilities, make sure you already have Apple Developer Account, then simply run this script and you can find the output file in build/ios/ folder

flutter build xcarchive --flavor prod -t lib/main_prod.dart

Would you like to proceed with setting up flavors in Android? Let’s move on to the next step.

https://ahmedyusuf.medium.com/how-to-build-flavor-in-flutter-android-with-different-firebase-config-96b259e5572e

Github Project

https://github.com/c0deslinger/flutter-learn-flavor

--

--