Flutter iOS — Setup Flavors with different Firebase Config
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.
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
- 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
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.
Github Project