Flavor Flav - Android Product Flavor Best Practices

...I think

Posted by Grego on October 6, 2016

ridiculous article header image featuring flavor flav

Preface

During my time at one of my previous employers we were gifted the opportunity of working with native iOS and Android applications for the first time. The previous hybrid cordova app that we had just worked on was already a huge mess and we were looking forward to getting our hands on some all-new Objective-C and Java code.

Like many in the field, over time we found ourselves burdened by the constant loading, removing and re-loading of our mobile apps onto our test devices during the development cycles. It became difficult managing different versions of the app and being able to tell at a glance when the dev code or production code was present on our devices. If only there was something we could have used for exactly that…

Actually, there was!

The problem, in a nut shell

Here are the 4 main problems we are trying to solve:

  1. Have multiple app versions loaded on a phone simultaneously, based on environment or branch (dev, staging, beta, production).
  2. Have distinct launcher icons for each environment or branch
  3. Have distinct launcher text to label each environment or branch (My App Dev, My App Beta, My App)
  4. Have the awareness of which environment or branch we are on to make different decisions at runtime based on the current environment.

Continue reading down the page if you want an explanation of things. If this document is too long you can check out the TL;DR Cheat Sheet below.

What’s a Product Flavor?

flavors!

Image credit: nutrawiki.org

From Android User Guide’s Configure Your Build:

Product flavors represent different versions of your app that you may release to users, such as free and paid versions of your app. You can customize product flavors to use different code and resources, while sharing and reusing the parts that are common to all versions of your app. Product flavors are optional and you must create them manually. To start creating different versions of your app, learn how to Configure Product Flavors.

A product flavor is just a variant of your app. As suggested here, the most common use of this tool is probably to have a free and paid version of the same app.

How is that different from a build type?

Well ultimately, it’s up to you how you want to use build types and build flavors. I would consider a build type to be something like debug vs release where a debug build would provide lots of debug output and a release build would have none. The debug build would allow things like a debug menu to configure the app state manually for testing where as a release version would not.

* Please note having a debug screen in your app could be a huge security flaw, if you decide to add one it would probably be best to make sure that code never gets compiled into a release build, a la pro guard

From the Android User Guide:

Build types define certain properties that Gradle uses when building and packaging your app, and are typically configured for different stages of your development lifecycle. For example, the debug build type enables debug options and signs the APK with the debug key, while the release build type may shrink, obfuscate, and sign your APK with a release key for distribution. You must define at least one build type in order to build your app—Android Studio creates the debug and release build types by default. To start customizing packaging settings for your app, learn how to Configure Build Types.

From this documentation it would seem that the better way to go about this is by using build types, but it’s important to keep in mind that the product flavor is essentially another dimension for your build types.

Each product flavor has one of each type of build.

For example, if you had product flavors called Free and Paid and you had two build types, debug and release you would essentially have a matrix of builds that looks like the following:

Flavor Build Type
Free debug
release
Paid debug
release

This means that the Free version has its own set of debug and release builds and the Paid version has its own set of debug and release builds.

Given the kind of flexibility this provides I see this only as a benefit to be able to have a debug and release build for each type of flavor.

How does that help me?

We can use product flavors to simulate different app states in our multi-stage environment. We can even use these flavors to set different flags so that we know which version of the app we are on from the code.

Here is my proposed environment matrix:

Flavor Build Type
dev debug
release
stage * debug
release
beta * debug
release
prod debug
release

* Depending on your app and environment you may or may not need these, you can add or remove as you see fit

Great. How do we do it?

The way to create a new flavor is by adding a productFlavor key to the app gradle file. However just adding a new productFlavor will not change anything on its own.

Loading multiple versions side-by-side

In order to be able to load these multiple flavors onto our device side-by-side we need to change the applicationId. Luckily Android provides us with a simple way of doing that. We actually have two options:

  1. Change each flavor to set a full applicationId
  2. Use the applicationIdSuffix option to append something unique to the end of our application id.

Since the introduction of the Gradle system, the android application id is now separate from the package name which you configure when you first create the project. This is great because it provides a separation between the android package name, an identifier used by the Java source code and the application id, the identifier used to uniquely identify your compiled application.

It’s generally cleaner to go with the latter (suffix) option unless you have a really good reason not to (personally, I can’t think of any but I’m sure there’s one out there).

Let’s add the new product flavors with their applicationIdSuffixes to our main app gradle:

android {
    defaultConfig {
        // generally the full application id is specified under defaultConfig
        applicationId "com.mysite.myapp.android"
        // ... other settings ...
    }

    buildTypes {
        // some build type stuff here, usually debug and release are set.
    }

    //this is the new stuff we're adding
    productFlavors {
        dev {
            applicationIdSuffix ".dev"
        }

        beta {
            applicationIdSuffix ".beta"
        }

        prod { }
    }
} // android

new build variants available

The prod flavor is empty for right now because we aren’t setting an application suffix—and we really shouldn’t! As with any gradle file change you’ll have to run a gradle sync after adding the flavors. You’ll notice new build selections in the Build Variants side bar.


Changing app icons (resources) based on flavor

different app icon per flavor

In order to be able to use a different app icon based on flavor all you need is to set up your resource directories accordingly. This also allows us to have any kind of resource separate for different flavors. Images, strings, values and so on.

Since directory structures can get confusing and complicated I recommend doing this through Android studio, but I’ll show the directory structure in a hot minute for those who don’t use AS.

  1. In the Android project view area, right click on res, select New → Android resource directory new android resource directory
  2. Select mipmap as the type and select the flavor you want to target as your source set, for example: dev. new android resource directory dialog
  3. Copy the icons using your system file browser or command line. Refer to the image below for the directory hierarchy copying icons to source directory Here I simply copied all the mipmap- directories under res.
  4. Compile and run any of your flavors and you’ll see the new icons appear:

Distinct launcher text

distinct icons and text

Whether you decide to change the icon or not for different versions is up to you. The next level of distinction between the different variants of your app is the launcher text that appears.

There are a few ways to do this. For example you could create a new strings.xml resource file in your variant and add an app_name string or some such.

The way I prefer to tackle this is using manifest placeholders by adding some extra variables into my gradle file under the productFlavors:

Updated 2017-03-06

I recently realized that by setting the appLabel to a string literal we are eliminating the use of the strings file. Most likely for local/internal development this is fine. For the release builds, I've updated the snippets below to use the strings file i.e., @string/app_name instead of a string literal, like My App Name.

android {
    defaultConfig {
        applicationId "com.mysite.myapp.android"
        manifestPlaceholders = [ appLabel: "@string/app_name" ]
    }

    productFlavors {

        dev {
            applicationIdSuffix ".dev"
            manifestPlaceholders = [ appLabel: "MyApp (Dev)" ]
        }

        stage {
            applicationIdSuffix ".stage"
            manifestPlaceholders = [ appLabel: "MyApp (Stage)" ]
        }

        beta {
            applicationIdSuffix ".beta"
            manifestPlaceholders = [ appLabel: "@string/app_name_beta" ]
        }

        prod { }
    }
} // android

Then we can use these placeholders we created in the AndroidManifest.xml:

<manifest ...>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="${appLabel}"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

Now build and run and you should see the new text appear.

Distinguishing between versions programmatically

flavor awareness

Now that we have our separate icons, app text and we can load different variants on the phone all at the same time, the final trick here is to be able to distinguish between these versions at run time. For the most part I don’t think do anything radically different logic wise in my apps, but for example if you did have a paid which gives you more content or removes ads, it could be helpful to have that kind of control flow.

Through the BuildConfig object we get a FLAVOR constant for free. Any time we access BuildConfig.FLAVOR we get the flavor name (key) from the gradle file:

Log.d(MainActivity.class.getSimpleName(),
        String.format(Locale.US, "Running %s flavor on %s build type.",
                BuildConfig.FLAVOR, BuildConfig.BUILD_TYPE));

Will produce:

D/MainActivity: Running stage flavor on debug build type.

You can also add extra build config fields in the gradle file using buildConfigField in the flavors:

android {
    productFlavors {
        stage {
            buildConfigField "boolean", "STAGE_ONLY_THING", "true"
        }
    }
}

and then in java:

if (BuildConfig.STAGE_ONLY_THING) {
    Log.d(MainActivity.class.getSimpleName(), "Staging rules!");
}

The only drawback with this is that you have to define the same field for every flavor. So I’ve yet to find a good use for this, but I can see how it could be useful. The buildConfigField is a function—I think—which takes 3 parameters: data type (String), identifier name (String) and value which is strangely also a String. So for example if you wanted to declare an int constant for the number of retries in your game before a game over:

buildConfigField "int", "MAX_RETRIES", "3"

TL;DR Cheat Sheet

build.gradle (app):

android {
    defaultConfig {
        applicationId "onebigfunction.com.flavorflav"
        manifestPlaceholders = [ appLabel: "Flavor Flav" ]
    }

    buildTypes {
        // defines debug/release config
    }

    productFlavors {

        dev {
            // add suffix to allow multiple app installs side-by-side
            applicationIdSuffix ".dev"
            // changes the name of the launcher text / activity in AndroidManifest.xml
            manifestPlaceholders = [ appLabel: "Flavor Flav (Dev)" ]
        }

        stage {
            applicationIdSuffix ".stage"
            manifestPlaceholders = [ appLabel: "Flavor Flav (Stage)" ]
        }

        beta {
            applicationIdSuffix ".beta"
            manifestPlaceholders = [ appLabel: "Flavor Flav (Beta)" ]
        }

        prod {
        }
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest package="..."
          xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- ${appLabel} is defined in the build.gradle manifestPlaceholders -->
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="${appLabel}"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

I’ve created a sample project as well to demonstrate.