Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Android Product Flavors and Configuration

5.00/5 (4 votes)
14 Apr 2016CPOL7 min read 40.9K  
This article deals with Android product flavors in depth. Learn how to create multiple apks from the same source code. Create, customize, group, configure and filter product flavors.

Introduction

Product flavors have been around for quite some time now. I feel Product flavors are one of the coolest things about Android studio. Product flavors lets us create multiple versions of our app like free, paid, demo, etc. (like angry birds free and angry birds hd) without making multiple copies of the source code. This article will help you understand application, grouping, configuring and filtering product flavors.

Background

Basic knowledge of Android development is a pre-requisite for this article. Don't worry if product flavors sound alien to you, this article will teach you all about it.

Defining Product Flavors

Product flavors are defined in Android block of app modules build.gradle file. This is how it can be defined:

Java
android {...
    defaultConfig {...}
    productFlavors {
        flavor1 {...}
        flavor2 {...}
        flavor3 {...}
    }
    ...
    buildTypes{...}
}

As you can see, this skeleton goes in Android block in build.gradle file. By writing this, we have created 3 flavors called flavor1, flavor2 and flavor3. By default, Android has 2 debug types called debug and release (more can be added like jnidebug), a build variant or variant is a combination of build type and product flavor. So now there will be 6 build variants: flavor1-debug, flavor1-release, flavor2-debug, flavor2-release, flavor3-debug and flavor3-release. Inside these flavor blocks are flavor specific properties and methods like applicationId (the app package name), versionCode, buildConfigField etc. We will see these a little later. Click here to see ProductFlavor DSL (domain specific language) object that is used to configure flavor specific properties and methods.

Now, we all know about default config, we specify properties like minSdkVersion, versionCode etc. in this block. Whatever properties and methods are defined in default config are inherited by all product flavors. Default config block also uses ProductFlavor DSL (domain specific language) object. This means that everything that goes inside a default config block can go inside a flavor block. Each product flavor can override the properties and methods defined in default config block. Here is a sample skeleton:

Java
android {
    ...
    defaultConfig {
        applicationId "the.default.packagename"
        minSdkVersion 8
        versionCode 10
    }
    productFlavors {
        flavor1 {
            applicationId "the.default.packagename.flavor1"
            minSdkVersion 15
        }
        flavor2 {
            applicationId "the.default.packagename.flavor2"
            versionCode 20
        }
        flavor3 {...}
    }
    ...
    buildTypes{...}
    ...
}

In the above example, some properties are overwritten in flavor blocks and the rest are inherited from default config block. Click here to see list of all the properties and methods that can be configured inside default config and flavor block (or groovy closure). It is a recommended practice to assign different applicationId to each flavor. It is used to assign different package name identifier to each flavor. This makes sure that different variants (or versions) of your app can be installed on the same device. In the above example, apks produced by flavor1, 2 and 3 can be installed on the same device.

SourceSets for Product Flavors

Configuring 3 product flavors as above creates 6 source sets:

Java
"src/flavor1" - android.sourceSets.flavor1
"src/flavor2" - android.sourceSets.flavor2
"src/flavor3" - android.sourceSets.flavor3
"src/androidTestFlavor1" - android.sourceSets.androidTestFlavor1
"src/androidTestFlavor2" - android.sourceSets.androidTestFlavor2
"src/androidTestFlavor3" - android.sourceSets.androidTestFlavor3
 
// All these folder follow the java/src/main/.. structure 
// for any flavor and test specific customization.

To know more about Android sourceSets, check out http://goo.gl/NvAg74.

This flavor specific folder structure is where the flavor specific code resides. This is not generated by default, you have to create the structure exactly as the main structure, if you want to keep your own structure, just override sourceSets in build gradle file (see http://goo.gl/NvAg74). flavor1 customization must be done inside “src/flavor1/ {java, res, assets}” folder. The common code lives in “src/main/…” directory. If you want to change the app name for say flavor2, all you need to do is define app_name (or whichever string resource you are using as app name in manifest) in “src/flavor2/res/values/strings.xml“. Just whatever you want to override, do not copy the entire XML or res file in flavor specific folder, let Android resource merger take care of it. Say you are using “@drawable/ico_app or @mipmap/ico_app” as app icon, this can be easily configured for each flavor by keeping flavor specific icons in respective folder structure. e.g. for flavor3, just name the flavor3 specific icon as ico_app and keep it in drawable or mipmap folder (whichever you are using) in flavor3 specific directory.

Multi-flavor Variants

Now whatever we have done till now is useful when the variants of your app is decided by one dimension. e.g. say the dimension is price, you can create flavors like free, paid etc. what if the requirement is to create variants based on multiple dimensions, like for environment dimension there can be three flavors “dev“, “staging” and “production“, and three for price dimension “free“, “freemium” and “paid“, and may be another dimension. In this case, you can select either of the six flavors plus debug or release (the default build types). But the product flavor we are interested in is dependent on both dimensions. Something like free-dev-debug, paid-production-release etc. Here, we are trying to group product flavors, which is not allowed by default. This can be enabled via dimension attribute of product flavors. We can set dimensions of product flavors via flavorDimensions attribute of android block, and then we can assign a dimension to each flavor. Here is an example:

Java
android {...
    flavorDimensions "country", "price"
    productFlavors {
        free {dimension "type"...}
        pro {dimension "type"...}
        India {dimension "country"...}
        China {dimension "country"...}
        Russia {dimension "country"...}
    }
    ...
    buildTypes{...}
}

In this example, as we can see, there are two flavor dimensions, country and price. Country flavor dimension has three flavors India, China and Russia, price has two flavors free and pro. This in turn creates 12 build variants for us:

India-free-debug
India-free-release
India-pro-debug
India-pro-release
China-free-debug
China-free-release
China-pro-debug
China-pro-release
Russia-free-debug
Russia-free-release
Russia-pro-debug
Russia-pro-release

As you can see, just by defining the flavorDimensions and dimension attribute, the product flavors can be grouped, and Android creates variants that are all possible combinations of flavors of all types of dimensions and build types. These variants are reflected everywhere, including the build variants tab on lower left side of android studio.

Filtering Product Flavors

Now the list of build variants above is all generated by Android. What if we want to filter this list. Say I don’t want Russia-free and India-paid variant for some reason. This is possible by ignoring some of the build variants based on some condition. Below is an example of ignoring Russia-free variant.

Java
//Filtering variants
android {
...
    variantFilter { variant ->
        def names = variant.flavors*.name
        def buildTypeName = variant.buildType.name
        // if buildtype is required for filtering use
        // the above field
        if (names.contains("Russia") && names.contains("free")) {
            variant.ignore = true
            // or variant.setIgnore(true)
        }
    }
    ...
}

The build variants can be ignored by setting the ignore field or by setting setIgnore method to false. This global filtering is reflected everywhere including the build variants tab on lower left side of Android studio, assemble, install gradle tasks, etc. The above code basically loops through all the variants and ignores some of the variants based on our custom condition.

Flavor Specific Dependency

You must have seen testCompile “junit…” in your projects. This is flavor specific dependency. junit for example is a testing library and must not be shipped with the release apk (why increase the size of your apk with something that is required only for testing). Adding “flavorCompile” syntax in dependency section of app level build.gradle adds the dependency for that particular flavor. If you are familiar with facebook’s stetho library, then you know it is only for development purpose and must not be shipped with release version. This is an awesome feature that comes with Android product flavors. Some examples:

Java
testCompile 'junit:junit:4.12'
stethoFlavorCompile 'com.facebook.stetho:stetho:1.3.1'

BuildConfig Constants and Resource Values

Now we know that flavor specific code goes in flavor specific sourceSet, but sometimes we need flavor specific code in main code base (“src/main/…“). The problem here is: the main source code doesn’t know which flavor is or will be generated in future. For this, there is something called BuildConfig. It is an auto generated file and must not be tempered with. This file contains flavor specific constants and can be used in main source code. Check out ProductFlavor DSL object for available properties and methods. You can set flavor specific constants and resources like this:

Java
android {...
    flavorDimensions "country", "price"
    productFlavors {
        free {dimension "type"
            buildConfigField("String", "featureList", "restricted")
            resValue("boolean", "ads", "true")
        ...}
        pro {dimension "type"
            buildConfigField("String", "featureList", "all")
            resValue("boolean", "ads", "false")
        ...}
       India { applicationId "my.app.india"
            dimension "country"
            buildConfigField("String", "shortcode", "IN")
       ...}
       China { applicationId "my.app.china"
            dimension "country"
            buildConfigField("String", "shortcode", "CHN")
       ...}
       Russia { applicationId "my.app.russia"
            dimension "country"
            buildConfigField("String", "shortcode", "RU")
       ...}
    }
    ...
    buildTypes{...}
}

This is then used to generate BuildConfig class and dynamic resources (check out resources in generated folder). For a Russian-pro-debug build variant (from flavor definition above), the generated build config will look something this:

Java
//Build config for Russia-pro-debug build variant
public final class BuildConfig {
    public static final boolean DEBUG = Boolean.parseBoolean("true");
    public static final String APPLICATION_ID = "my.app.russia";
    public static final String BUILD_TYPE = "debug";
    public static final String FLAVOR = "proRussia";
    public static final int VERSION_CODE = 1;
    public static final String VERSION_NAME = "1.0";
    public static final String FLAVOR_price = "pro";
    public static final String FLAVOR_country = "Russia";
    // Fields from product flavor: pro
    public static final String featureList = "all";
    // Fields from product flavor: Russia
    public static final String shortcode = "RU";
}

As you can see, the constants are specific to a selected flavor. This can then be used in the main source. Easy peasy isn’t it? Build config file is in this location (“<app or module>\build\generated\source\ buildConfig\<variant>\<debug or release>\my\app\package“).

Dynamic Manifest

Sometimes, we need to use app’s package in manifest file. But with product flavors, the app’s package or application id is no longer fixed, it changes with selected flavor. In this case, we can make the package dynamic in manifest with groovy syntax. By doing so, the application Id is picked from build.gradle at run time. Here is a sample:

XML
<manifest xmlns:android="http://schemas.android.com/apk/res/android......
...
<permission 
    android:name="${applicationId}.permission.C2D_MESSAGE"
    android:protectionLevel="signature" />
...
<application...

This is an example for a declared permission for Android GCM. Notice the groovy syntax around applicationId, that is where the magic is happening.

Points of interest

If in multi-flavor variants, there is a conflict in constants or package name or in resources, the general rule to keep in mind is "concrete flavor overwrites general flavor". For example, app icon defined in free flavor will be overriden by icon defined in India-free flavor whenever India-Free-Debug variant is generated.

To generate debug apks of all variants, just open the gradle tab on right hand side of Android studio, look for task called "assembleDebug" and double click it. Or simple type "gradlew -q :app:assembleDebug" in Android studio terminal.

History

  • 1.0 - Base version by Kaushal Dhruw
  • 1.1 - Removed confusing terms flavor and flavours.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)