paint-brush
Flutter Flavoring 101by@r3tam
1,391 reads
1,391 reads

Flutter Flavoring 101

by Artem ZaytsevJuly 7th, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Head of Flutter Department at Surf.com. End-to-end mobile app development for fintech, foodtech and e-commerce. Surf is Surf's largest mobile app developer in the world, based in New York City, New York. Surf's mobile app team is based in the U.S. based in San Francisco, California, New Jersey and New York, USA. Surf’s mobile app app development is surfacing in the US and based in California, California.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Flutter Flavoring 101
Artem Zaytsev HackerNoon profile picture

Why do you need flavors? Let’s say there’s an app with integrated analytics. This means there are developers, testers, and 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 the 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:

Android

IOS

build types

build configurations

flavors

targets

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 the dev version differs by having a suffix.


Solution:

  • Create two configurations.
  • Add a suffix to the dev one.
  • 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=.dev
IDENTIFIER=$(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 parameters — I’ll explain them a bit later.


Create two configurations and add them to the following directories:

ios/Flutter/dev.xcconfig

ios/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 copying
GOOGLESERVICE_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 exists
echo “Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_DEV}”
if [ ! -f $GOOGLESERVICE_INFO_DEV ]
then
echo “No Development GoogleService-Info.plist found. Please ensure it’s in the proper directory.”
exit 1 # 1
fi
# Make sure the prod version of GoogleService-Info.plist exists
echo “Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_PROD}”
if [ ! -f $GOOGLESERVICE_INFO_PROD ]
then
echo “No Production GoogleService-Info.plist found. Please ensure it’s in the proper directory.”
exit 1 # 1
fi
# Get a reference to the destination location for the GoogleService-Info.plist
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
if [[ “${CONFIGURATION}” == *-prod ]]
then
echo “Using ${GOOGLESERVICE_INFO_PROD}”
cp “${GOOGLESERVICE_INFO_PROD}” “${PLIST_DESTINATION}”
else
echo “Using ${GOOGLESERVICE_INFO_DEV}”
cp “${GOOGLESERVICE_INFO_DEV}” “${PLIST_DESTINATION}”
fi

An afterthought

Well, these rather tricky manipulations helped us to set up flavors. This is a fundamental guide to setting them up. In Surf, we automated part of this process not to do it with each new project. Perhaps sometime in the future, the Flutter team will make full-fledged tooling for creating flavors. But for now, it is what it is. And making life easier is in our own hands.


Also published on Medium’s subdomain.