Multiple Build Variants in Android with common source code

Sonika Srivastava
hubbleconnected
Published in
4 min readDec 10, 2021

Hubble Connected, ISO 27001:2013 certified & GDPR compliant is a connected, digital baby tech & smart living IOT platform that empowers first time parents and families to access even the most complex technology with ease. Hubble’s ambition is to get users connected and become their smart & reliable companion during their life journey; from pregnancy, through birth, toddler years, to building a bigger family and beyond.

Hubble, not only has its own app for supporting its suite of monitoring and wellness devices, it also supports other brands of baby monitors to help their customers stay connected. Hence there is a need for multiple apps with a common source code.

Build variants help us to build different versions of the app with a common source code. e.g., We may want a free and a paid version of the app, or we may want to support multiple clients with similar requirements, but different app themes. Let us understand, how we can achieve the same.

Firstly, let us understand the common nomenclature.

Build Types:

Build Types can be configured in the module-level build.gradle file. Debug and Release are most common build types. In-fact, Android studio automatically creates Debug and Release build types whenever a new module is created.

buildTypes {
debug{
debuggable true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

Product Flavors:

Product Flavors help us to create different variants of the app, with the same source code. e.g., We might want a staging and a production version of the app, which points to staging and production server respectively. Or, we might want to have a free and paid version of our app, where free version has limited set of features and paid version has full set of features.

flavorDimensions "main"

productFlavors {

free {

dimension "main"

applicationIdSuffix ".free"

}

paid {

dimension "main"

applicationIdSuffix ".paid"

}

}

Build variant is nothing but a Cartesian product of build type and product flavors.

Note, if we need these build variants as separate app listing in PlayStore, we need a unique applicationId for each variant.

Flavor Dimensions

If we need to combine configurations from multiple groups of product flavours, we can use flavour dimensions.

e.g. We can group our build variants based on the server it points to i.e. staging or prod, for each of the product flavours.

flavorDimensions "main", "server"

productFlavors {

free {
dimension "main"
applicationIdSuffix ".free"
}

paid {
dimension "main"
applicationIdSuffix ".paid"
}


staging{
dimension "server"
}


prod{
dimension "server"
}

}

Adding a new flavorDimension, will increase the number of build variants by creating a cartesian product of build type, product flavor and flavor dimension.

Thus, we will get the following build variants:

Now that we have understood all the terminologies, let us try to solve our problem of catering to multiple clients with same source code.

If we have 3 clients, it is straight-forward that we can create 3 product flavors as follows:

flavorDimensions "main"

productFlavors {
client1 {
dimension "main"
applicationIdSuffix ".client1"
}

client2 {
dimension "main"
applicationIdSuffix ".client2"
}


client3 {
dimension "main"
applicationIdSuffix ".client3"

}

}

With this configuration, we can have the following code structure:

The source code common to all variants go in the main folder. Source code specific to the clients go in respective client folder. Please note, there can be resource files with same name in main and client folders, but not class files (Java/Kotlin].

Though this seems straight-forward, it becomes non-trivial when the number of clients increase.

Challenge:

Say, we need some modules, which are required by more than 1 client but not all clients i.e. We cannot move the source code to main because all clients don’t need that code and we cannot have duplicate code in both clients. So, what is the solution for this?

Solution:

Source sets:

The main source set and directories is created by Android studio for everything that needs to be shared between all build variants. However, we can also create new source sets to control exactly which directories need to be compiled for which build type, product flavors and build variants.

This way we can make our modules configurable for different clients and we can easily add/remove any module from any variant.

Source sets can be defined in the following way:

sourceSets {

client1 {
manifest.srcFile 'src/client1/AndroidManifest.xml'
java.srcDirs += 'src/client1/java'
resources.srcDirs += 'src/client1/res'
}

client2 {
manifest.srcFile 'src/client2/AndroidManifest.xml'
java.srcDirs = ['src/client2/java', 'src/common/java', 'src/client2/kotlin']
res.srcDirs = ['src/client2/res', 'src/common/res']
}

client3 {
manifest.srcFile 'src/client3/AndroidManifest.xml'
java.srcDirs = ['src/client3/java', 'src/common/java', 'src/client3/kotlin']
res.srcDirs = ['src/client3/res', 'src/common/res']
}
}

As we can see, client2 and client3 can share a common source set, without adding it in main, since it is not required by client1.

Specifying source sets in this way gives us lot of flexibility to add new features/modules, while avoiding duplicate code and keeping the code clean and modular.

Summary:

Thus, build variants and source sets provides us with a clean and scalable way to support various versions of the app with the same source code.

--

--