Flavors in Flutter - Multiple apps from one source code

 What is Flavor

Flavors are just comfortable. They let you define some build configurations and switch them as you want. For example, you could have one flavor for development, and one for production. You can set different URLs for API calls or different icons and app names. With a click, you’re ready to develop or release your great application.

We use a lot of flavor configurations, not only for comfort but also to be sure to deploy the right version of the app. We just set different icons and sometimes even app themes to be sure not to release an app with an API URL pointing to local or staging machines. That would be very dangerous and shameful.

Usually, we use from 3 to 5 different flavors per app (development, production, staging, demo..)

Flavors in Flutter

When it comes to Flutter, flavors are easy to implement, but if you don’t know android flavors and iOS schema you can have some problems. I used to use android buildConfigs and iOS targets and so it was not so straightforward.

Flutter cli has an option for flavors, so you can run/build your app using flavors:

flutter build --flavor development


These are mapped to android product flavors and iOS schema.

The approach for this tutorial

I am going to build a sample app with two flavors: dev, prod.

Step 1: Initialize a default flutter app

Run flutter create flavor_test to create a default flutter project. Nothing fancy.. this is the first commit of the sample app.

default flutter project
create a new Firebase project called Flavor Test Dev
Create the app for iOS and Android in Firebase console and download the GoogleService-Info.plist and google-services.json respectively. 

An XCode peculiarity for Android guys:

You’ll have to add the ios file through XCode, as the project file does not detect it if you simply copy it to the ios/Runner directory via the command line. Drag and drop it in the Runner folder in XCode, and select all targets. I learned this only while I was writing this tutorial, so thought it might be helpful to someone setting this up from scratch.

In this step, I have also updated the main.dart file to store the counter in the Firebase Realtime DB instead of just locally. Make sure you set up the Firebase Realtime DB via the console and add security rules to allow write access. 

This is what the app looks like after this commit -

the counter is now connected to Firebase Realtime Database

You can look at the commit diff here to see what all changed —

Code Diff : Step 1 to Step 2https://github.com/animeshjain/flavor_test/compare/step_1_init...step_2_firebase
We are ready with a basic counter app. However, when we ship this app, it will connect to the same firebase database, as it does when we are building and testing it locally. This is a problem, and this is what flavors are supposed to solve.
The current Firebase project we created was for our dev environment (it was named as such as well), and now we want to create a flavor which will be for our prod environment.
So we can create a new Firebase project for the prod environment
Dev and Prod are separate projects, so that they can have separate realtime DB instances
Also create an Android app in the project and download the google-services.json file and keep it handy. We will shortly add it to our app.
For kicks, let’s just see what flutter says when we try to run our app using the --flavor flag.
So this gives us a hint that we need to add product flavors to our build.gradle

We add flavors to app/build.gradle and it looks something like this

adding product flavors dev and prod to the app/build.gradle
The dev flavor will use the default applicationId as com.kanily.flavortest and the prod flavor will use the flavor specific applicationId as defined in the prod flavor definition com.kanily.flavortest.prod. We have also defined a string resource called app_name which we are using in the AndroidManifest.xml instead of hard coding the app name. Finally, regarding the google-services.json, it can be put under the source folder within subfolders with names matching the flavors. From Firebase Docs -
https://firebase.google.com/docs/projects/multiprojects

So these settings take care of

  • Different app id’s for different flavors, so that all flavors can be installed simultaneously on the device
  • Different application names for different flavors, so that users/testers/developers can easily differentiate
  • Each flavor pointing to its own Firebase project (this is automatically taken care of by the convention of placing the files in folders names the same as flavors)

Now you can run the app using the commands

flutter run --flavor dev or flutter run --flavor prod

Both apps can be installed on the device now and they can run in parallel.

Notes: For simplicity, I have removed the profile and release directories which were there in the default flutter scaffold project. They were only adding the Internet Access permission via AndroidManifest.xml, which I have added to the main file.

Here is the code diff to see what all changed —

Code Diff : Step 2 to Step 3https://github.com/animeshjain/flavor_test/compare/step_2_firebase...step_3_android_flavors

iOS flavors are going to be more nuanced to setup. Also, iOS configuration is mostly done using the XCode UI and not by editing config files in a text editor😱, it is quite messy to explain by typing words. But I have you covered, to help with understanding I’ve recorded all actions and embedded the screencast gifs. So have a glass of water and buckle up…

Let’s just start with running the flutter run command targeting an iOS device/simulator and see what happens

flutter run --flavor dev

Xcode has the concept of schemes and build configurations as a parallel to product flavors in Android
So we need to setup something called custom schemes it seems. We’ll need to fire up Xcode and openios/Runner.xcworkspace
And then, here’s how to setup a custom scheme called dev…
Create a new scheme called “dev”

Now running flutter run --flavor dev again…

So the error message tell us that Flutter expects a build configuration by the name of Debug-dev or similar. Let’s create these build configurations…

Duplicate build configuration to create “Debug-dev” and similar

Trying flutter run --flavor dev again…

So this worked. But right now we have not customized anything in the build scheme / build configuration, so the app is running with the same configuration as it was earlier. Let’s rename the default build scheme and build configurations to prod.

Rename default scheme and build configurations to “prod”

Since we duplicated the build configurations, the dev ones are still connected to the original scheme (which we have now renamed to prod). Lets fix that as well…

Connect “dev” build configurations to “dev” scheme

Now we have two schemes connected to their own build configurations. We can now customize things per scheme. Let’s first change the app bundle identifier to be different for both schemes. Our prod applicationId in Android was com.kanily.flavortest.prod, bundle identifier in iOS is parallel to the applicationId in Android. So let’s change our prod bundle identifier to com.kanily.flavortest.prod

Update Bundle Identifier for prod build configurations

We would also like to use different Display Names for the app. However, the Display Name parameter is not there in the Build Settings for the target, so we simply make a user defined parameter, and use it instead…

[UPDATE May 5, 2020. Thanks to Stanford Lin for the comment]

There has been a change in how Flutter ios build works now, and this video is a bit outdated. You will probably get an error that says

Could not find the built application bundle at build/ios/iphoneos/Runner.app

To avoid this error, instead of adding the `$(APP_DISPLAY_NAME)` to the Display Name property in General Settings tab, you should update your Info.plist file to include a new property

<dict>
...
<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>

...
</dict>

You do not need to change the Display Name property in General Settings tab any more.

Set a custom App Display Name per build configuration
Finally we have to figure out a way to use a different GoogleServices-Info.plist based on the build configuration. There are some solutions which suggest doing this at runtime on app startup, basically initializing Firebase by explicitly specifying which config file to use (the Firebase docs suggest the same — https://firebase.google.com/docs/projects/multiprojects). But I like the other option of copying the right file at the default location at build time, so that when the app bundle gets generated, it used the right file automatically.
To achieve that, first we will keep the GoogleServices-Info.plist files for each flavor in a separate folder like follows…
Copy GoogleServices-Info.plist files to a separate config directory, and link to that directory in XCode

Make sure you drag and drop the config folder explicitly into XCode after copying it to the correct location from the command line or finder. XCode only adds it to it’s project reference after you add it explicitly. It does not pick up files/folders kept in the project directory by default. XCode folder structure after following the above steps looks like…

Now we need to figure out how to add a step in the build process so that the respective GoogleServices-Info.plist file is copied into the correct location, i.e. inside the Runner directory. This can be accomplished by adding a new Run script Build Phase to the target…

Create a Run Script build phase to copy GoogleServices-Info.plist to the default location in app package

The script I used is below…


firebase.sh
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}"
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}"




For people who are using zsh instead of bash, you need to make a small adjustment to make the script run.

I had to wrap the script provided by the author with two lines. So the script becomes:

setopt KSH_ARRAYS BASH_REMATCH
<insert the author’s script>
unsetopt KSH_ARRAYS

I got it from here. Thanks.

And with this, we should be done! Now we can run

flutter run --flavor dev or flutter run --flavor prod

And the iOS device / simulator will install separate apps which connect to separate Firebase DBs!

Code Diff : Step 3 to Step 4https://github.com/animeshjain/flavor_test/compare/step_3_android_flavors...step_4_ios_flavors

Sometimes just different names are not enough. You might want different app icons as well for a more clear visual distinction when multiple flavors are installed in a device.

So far I was using the default launcher icon that Flutter comes with, but to illustrate the steps I have create two icons for prod and dev.

  • Steps for Android

Depending on which flavor you consider “default”, one of these icons can simply go to the android/app/src/main/res folder and replace the existing ic_launcher.png files for all the sizes. I’m going to put my dev icon into the default folder.

For the prod flavor, as some of you might have guesses, we will simply create a res folder in the prod folder, and add all size specific folders there. So your directory structure will look something like this

Note: You could have put your dev app icon files into the dev folder specifically as well. Android just looks into the flavor specific directory first, and if nothing is found there, then it falls back to the default directory called main and uses the resources found in that instead.

This is how the icons look on Android -

Both Flavors installed on the same device
  • Steps for iOS

We’ll jump back to Xcode for this one. We create app icon assets and then tell XCode to use different icons for different flavors. Check the gif…

Different AppIcons for different build configs in Xcode

Now when you build the app on iOS, you’ll get something like this -



Flutter Flavor With flutter_flavorizr Package

A flutter utility to easily create flavors in your flutter application

Side note: this tool works better on a new and clean Flutter project. Since some processors reference some existing files and a specific base structure, it could be possible that running Flutter Flavorizr over an existing project could throw errors.

Before running Flutter Flavorizr, you must install the following software:

These prerequisites are needed to manipulate the iOS project and schemes. If you are interested in flavorizing Android only, you can skip this step. Keep in mind that you will have to use a custom instructions set with Android and Flutter processors only, otherwise an error will occur.

Installation 

dev_dependencies:
  flutter_flavorizr: ^2.1.2

You can install packages from the command line:

pub get


Once all of the prerequisites have been installed and you have added flutter_flavorizr as a dev dependency, you have to edit your pubspec.yaml and define the flavors.

flavorizr:
  app:
    android:
      flavorDimensions: "flavor-type"
    ios:

  flavors:
    apple:
      app:
        name: "Apple App"

      android:
        applicationId: "com.example.apple"

      ios:
        bundleId: "com.example.apple"

    banana:
      app:
        name: "Banana App"

      android:
        applicationId: "com.example.banana"
      ios:
        bundleId: "com.example.banana"

Usage 

When you finished defining the flavorizr configuration, you can proceed by running the script with:
flutter pub run flutter_flavorizr

You can also run flutter_flavorizr with a custom set of processors by appending the -p (or --processors) param followed by the processor names separated by comma:

flutter pub run flutter_flavorizr -p <processor_1>,<processor_2>

Example

flutter pub run flutter_flavorizr -p assets:download
flutter pub run flutter_flavorizr -p assets:download,assets:extract

Run your flavors 

Once the process has generated the flavors, you can run them by typing

flutter run --flavor <flavorName> -t lib/main-<flavorName>.dart

Example

flutter run --flavor apple -t lib/main-apple.dart
flutter run --flavor banana -t lib/main-banana.dart

Customize your app 

Flutter_flavorizr creates different dart files in the lib folder. In the flavors.dart file we have the F class which contains all of our customizations.

class F {
  static Flavor? appFlavor;

  static String get title {
    switch (appFlavor) {
      case Flavor.APPLE:
        return 'Apple App';
      case Flavor.BANANA:
        return 'Banana App';
      default:
        return 'title';
    }
  }

}

The process creates simple title customization: a switch that checks the current appFlavor (defined in our app starting point) and returns the correct value. Here you can write whatever you want, you can create your custom app color palette, differentiate the URL action of a button, and so on.

If you are wondering how to use these getters, you can find an example under the pages folder: in the my_home_page.dart file, the page shown after the launch of the app, we can see a clear reference on the title getter defined in the F class.

Side notes 

I haven't found yet a good groovy parser to guarantee the idempotency of the AndroidBuildGradleProcessor.
The only way to keep track of the autogenerated flavor dimensions is to mark up the beginning and the end of the section with magic comments.
Please do not erase these comments otherwise you will break down the AndroidBuildGradleProcessor.

Third-party services 

Google Firebase 

In order to flavorize your project and enable Firebase in your flavor
you have to define a firebase object below each OS flavor. Under the
firebase object you must define the config path of the google-services.json
(if you are under Android configuration) or GoogleService-Info.plist
(if you are under iOS configuration).

As you can see in the example below, we added the path accordingly

flavorizr:
  app:
    android:
      flavorDimensions: "flavor-type"
    ios:

  flavors:
    apple:
      app:
        name: "Apple App"

      android:
        applicationId: "com.example.apple"
        firebase:
          config: ".firebase/apple/google-services.json"

      ios:
        bundleId: "com.example.apple"
        firebase:
          config: ".firebase/apple/GoogleService-Info.plist"

    banana:
      app:
        name: "Banana App"
        
      android:
        applicationId: "com.example.banana"
        firebase:
          config: ".firebase/banana/google-services.json"
      ios:
        bundleId: "com.example.banana"
        firebase:
          config: ".firebase/banana/GoogleService-Info.plist"

Update Flavor:

after updating flavor you need to call the below command

flutter pub run flutter_flavorizr -p android:buildGradle,android:androidManifest

If you use Firebase then put your diff-diff .json files in this path

\android\app\src\stest\debug\google-services.json      
\android\app\src\debug\stest\google-services.json      
\android\app\src\stest\google-services.json
\android\app\src\debug\google-services.json
\android\app\src\stestDebug\google-services.json       
\android\app\google-services.json



Post a Comment

Previous Post Next Post

Subscribe Us


Get tutorials, Flutter news and other exclusive content delivered to your inbox. Join 1000+ growth-oriented Flutter developers subscribed to the newsletter

100% value, 0% spam. Unsubscribe anytime