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:
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:
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:
"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
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:
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.
android {
...
variantFilter { variant ->
def names = variant.flavors*.name
def buildTypeName = variant.buildType.name
if (names.contains("Russia") && names.contains("free")) {
variant.ignore = 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:
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:
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:
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";
public static final String featureList = "all";
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:
<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.