Setting Up Flavors in Flutter

Артем Зайцев
Surf
Published in
8 min readJun 11, 2020

Why do you need flavors

Let’s say, there’s an app with integrated analytics. It has its devs, the test team, and its end users. All three groups use the same version of the app. One day, you decide to analyze users’ interest in feature A.

What do you do if that’s the case? You go to Analytics and check the total number of uses for that feature (say, screen views).

What you find there is that there have been numerous views, which is by no means possible, given the current audience reach. Besides, all these screen views were registered over a particular period of time.

You dig a little deeper and see that the feature was being tested at the time. And before that, the feature had been at the development stage. And that also involved collecting data for analytics.

What you get is inaccurate, low-quality analytics based on dirty data.

Replace ‘analytics’ with push notifications, crash reporting, etc.

The perfect solution to this problem would be splitting the app into two slightly modified versions, for example, with different Bundle IDs (package-names). Developers and testers use only the dev version, and users — the prod one.

That’s one of flavors’ use cases. In this article, I’m going to use the term ‘flavor’ since that is the name used by Flutter. People familiar with Android development, I think, would immediately recognize this mechanism.

Flavoring Flutter?

Well, we figured out WHAT you need to do. But HOW to do it? Is flavoring Flutter as simple as they say?

First, let’s define dealing with flavors as a purely native task. Information about them won’t be available from the dart code. That’s why we’ll turn to native mobile development for setup methods.

Android

It’s really straightforward and by no means different from usual methods on Android. You may ask yourself, why not use buildType, but we’ll get back to this later.

Here’s what you need at a minimum:

flavorDimensions “release-type”productFlavors {   dev {       dimension “release-type”       applicationIdSuffix “.dev”       versionNameSuffix “-dev”   }   prod {       dimension “release-type”   }}

That’s it! Now you can easily run this command

flutter run --flavor dev

on your Android device.

The most thoughtful developers may ask, ‘Why not use buildType?’. The answer is: The Flutter team has hardcoded buildType to their needs. Well, that is what the debug build magic is all about.

A note on build types and different configurations

So, we mentioned buildTypes. Let’s take a closer look at them and their iOS counterparts.

The following correspondence table can be made:

Here, build types and configurations are something that affects the build itself rather than the codebase or app differences (although debatable). While flavors and targets turn up to be quite a convenient tool for creating and configuring dev and prod versions with different settings for one app.

And everything would be fine and would set up just like that, but… There’s always a but.

The Runner target is hardcoded.

It turns out, using it to implement flavors in iOS is not possible. The thing is, the Flutter team has reserved the Runner target for their own needs. Feels like it’s time for us to wrap up and go home… But not yet. Because you can use build configurations.

iOS

Problem: You need to implement two flavors, for development and production, where dev version differs by having a suffix.

Solution:

  1. Create two configurations.
  2. Add suffix to the dev one.
  3. Profit!

Now let’s take a closer look.

Configuration files

There are two configurations in your projects: dev and prod. Their contents are as follows:

#include “Pods/Target Support Files/Pods-Runner/Pods-Runner.debug-dev.xcconfig”#include “Generated.xcconfig”#include “common.xcconfig”bundle_suffix=.devIDENTIFIER=$(identifier)$(bundle_suffix)

As you see, that’s where you set bundle_suffix.

By the way, Flutter itself has the Release and Debug configurations. You should add bundle_suffix there too. You don’t want your version to be the prod one by default when running it from your favorite IDE.

You can see some IDENTIFIER parameter — I’ll explain it a bit later.

Create two configurations and add them to the following directories:

ios/Flutter/dev.xcconfigios/Flutter/prod.xcconfig

This can also be done through Xcode (even better — to add them as configuration files). Right-click on Runner → New File → Configuration Settings File → select the save location.

Build Configurations. Make it double!

It’s time to get familiar with build configurations. In Xcode, open Runner.xcworkspace and select the Runner project.

Find ’+’ in the Configurations section and create four new configurations: two for Release and two for Debug adding a postfix with the name of your config and future app scheme.

Like this:

Unfortunately, duplication of configurations is still necessary, since iOS build script is very sensitive to naming.

Adding schemes

Apart from creating config files, you need to correctly configure application schemes — there will also be two of them.

This one is really easy. Important note: choose the correct target — Runner.

Now, select Edit Scheme and add the necessary configurations to each of the scheme processes.

Updating Info.plist

And the finishing touch (spoiler: still far from finished) — set the Bundle Identifier parameter in Info.plist as:

$(PRODUCT_BUNDLE_IDENTIFIER)$(bundle_suffix)

We’re all done… aren’t we?

You have configured everything correctly, the project runs smoothly, the Android setup was easy as pie… But if you suddenly decide to use fastlane gym for signing iOS — it just won’t work. And for some reason, iOS app signing is not working at all… Let’s find out why.

No Provisioning Profile

The first error you see while uploading — Xcode couldn’t find a provisioning profile. What is more, the identifier in the error is not the one you have set in the config.

It turns out, setting the identifier in Info.plist doesn’t work. Gym deals specifically with PRODUCT_BUNDLE_IDENTIFIER — and you have the same one for all configurations.

Remember the mysterious common.xcconfig file and the IDENTIFIER parameter? Those two are exactly what you need to solve this problem.

Let’s create another config file, in which you will set the basic part of your PRODUCT_BUNDLE_IDENTIFIER.

File contents are defined in a single line:

identifier=your.bundle.identifier

Include this file in other configs and set a new User Defined Variable IDENTIFIER:

#include “common.xcconfig”IDENTIFIER=$(identifier)$(bundle_suffix)

Now, let’s do some mouse work in Xcode. Select your target and click the Build Settings button:

Do a search for Product Bundle Identifier (the Packaging section):

Change values for all configs to:

$(IDENTIFIER)

Now go to Info.plist and remove bundle suffix from the identifier line, leaving only:

$(PRODUCT_BUNFLE_IDENTIFIER)

Try to build and sign. Now everything works fine…

Separate files for different Bundle IDs

…But you’ve decided to integrate analytics. If you use Firebase, you’ll need two projects and four apps respectively (two platforms for two versions).

Most importantly, you’ll need to have two google-services.json files (Google-Services.Info.plist). With Android, it’s easily managed: just create a folder with your flavor’s name and add your file there.

When it comes to iOS, get ready for an adventure with shell scripts and build phases.

Creating and locating files

You need to create a new folder in the project to store these files. Use the following structure:

Important note: do not create them via XCode. The files should not be mapped to the project. If Xcode is your favorite IDE, uncheck the Add to Targets checkbox when creating the files.

The next step is adding your files to the corresponding folders.

Adding files to the app at build time

Since the files are not mapped to the project, they won’t get into the target archive. You should add them here manually.

Add an extra build phase in the form of Run Script (let’s name it Setup Firebase, for example):

You need to pay attention to the location, it’s crucial.

Now, add the script. As an option, you can use the following one:

# Name of the resource we’re selectively copyingGOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist# Get references to dev and prod versions of the GoogleService-Info.plist# NOTE: These should only live on the file system and should NOT be part of the target (since we’ll be adding them to the target manually)GOOGLESERVICE_INFO_DEV=${PROJECT_DIR}/${TARGET_NAME}/Firebase/dev/${GOOGLESERVICE_INFO_PLIST}GOOGLESERVICE_INFO_PROD=${PROJECT_DIR}/${TARGET_NAME}/Firebase/prod/${GOOGLESERVICE_INFO_PLIST}# Make sure the dev version of GoogleService-Info.plist existsecho “Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_DEV}”if [ ! -f $GOOGLESERVICE_INFO_DEV ]thenecho “No Development GoogleService-Info.plist found. Please ensure it’s in the proper directory.”exit 1 # 1fi# Make sure the prod version of GoogleService-Info.plist existsecho “Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_PROD}”if [ ! -f $GOOGLESERVICE_INFO_PROD ]thenecho “No Production GoogleService-Info.plist found. Please ensure it’s in the proper directory.”exit 1 # 1fi# Get a reference to the destination location for the GoogleService-Info.plistPLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.appecho “Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}”# Copy over the prod GoogleService-Info.plist for Release buildsif [[ “${CONFIGURATION}” == *-prod ]]thenecho “Using ${GOOGLESERVICE_INFO_PROD}”cp “${GOOGLESERVICE_INFO_PROD}” “${PLIST_DESTINATION}”elseecho “Using ${GOOGLESERVICE_INFO_DEV}”cp “${GOOGLESERVICE_INFO_DEV}” “${PLIST_DESTINATION}”fi

An afterthought

Well, these rather tricky manipulations helped us to set up flavors. I do hope the solution is temporary, and we’ll soon be granted a new Flutter build system (the work is in progress). But for now, it is what it is. And making life easier is in our own hands.

--

--