Scaling Android Apps with Gradle! (Having different Source Sets)- Part 2

Sahil Jain
OLX Engineering
Published in
6 min readMay 31, 2022

--

This is in continuation to Part 1 of the two-part series on “Scaling Android Apps with Gradle!”. Whereas Part 1 describes configuring android builds with “Build Types”, “Product Flavors” and “gradle.properties”, Part 2 focuses on advanced aspects of gradle by fine-tuning the android builds by configuring different “Source sets” in your codebase. Although I have tried to make this post-self-sufficient, I would still recommend going through Part 1 to get a better context and background behind this post :).

Configuring different Source Set(s)

The majority of us normally deploy code into the “main/” (“test/” and “androidTest/”) source set that defines the code and resources which is to be shared among all the build variants. Additionally, Gradle also allows the creation of different source sets (apart from “main/”, “test/” and “androidTest/”) and controls exactly what files Gradle compiles and packages for specific build types, product flavors, and build variants. This is an extremely powerful feature that allows more flexibility in configuring the builds which otherwise couldn't have been possible by defining Gradle properties in “buildTypes{}” or “productFlavours{}” described in Part 1. Below table shows how code can be organized into different source sets :

Different source sets based on buildType, productFlavor, and buildVariant

Key Advantages

  • Helps organize files and resources that Gradle should use when building certain build variants of your app.
  • Compile only source code and resources required for a particular variant.

Building with different source sets

While compiling code and resources belonging to different source sets, it’s highly probable that two or more source sets contain resources with the same name. In case this happens, Gradle considers the below priority when deciding which file to use.

build variant (Highest priority) > build type > product flavor > main source set > library dependencies (Lowest priority)

Along with the above priority order Gradle considers the below rules while compiling any build variant:

All Source code in the java/ directories are compiled together to generate a single output. In case Gradle encounters two or more source-set directories that have the same Java class name, It throws a “Duplicate class” error.

Configuring source set based on “build types”

We can configure source sets based on build types like “debug” or “release” declared in the module level Gradle file. Eg. At OLX we use popular libraries like “Chucker” and “LeakCanary” which are super helpful during the development lifecycle and help in debugging and evading bugs and thereby enhancing the quality of the code before it is shipped to production. Below is the code snippet of how we declare these two dependencies in Gradle.

Declaring “leakCanary” lib for “debug” mode only
Declaring different “chucker” libs for “debug” and “release” variants

To access these dependencies from *.java or *.kt files, we need to deploy the controlling code inside “debug/” and “release/” source sets as these dependencies are not available in the “main/” source set. Below is the package structure we need to have:

Package structure to configure “debug” and “release” source sets

Below is the code for LibUtil.kt (You can keep any other name as well) inside the “debug/” source set which will be used by Gradle while generating debug build variants.

class LibUtil {    companion object {        @JvmStatic
fun initializeStetho(context: Context) {
Stetho.initializeWithDefaults(context)
}
@JvmStatic
@JvmOverloads
fun initializeLeakCanary(enable: Boolean, appContext: Context) {
logService.log("LeakCanary state :: isEnabled -> $enable")
AppWatcher.config = AppWatcher.config.copy(enabled = enable)
LeakCanary.config = LeakCanary.config.copy(dumpHeap = enable, onHeapAnalyzedListener = LeakUploader(appContext))
LeakCanary.showLeakDisplayActivityLauncherIcon(enable)
}
}
}

“initializeStetho()” method can be invoked from any class or method defined inside “main/” or “debug/” source set. This method will initialise “Stetho” at run time.

Below is the code for LibUtil.kt inside the “release/” source set which will be used by Gradle while generating release build variants.

class LibUtil {    companion object {        @JvmStatic
fun initializeStetho(context: Context) {
}
@JvmStatic
@JvmOverloads
fun initializeLeakCanary(enable: Boolean = false, appContext: Context) {
// This log is added just to suppress kotlin unused variable lint warning and this will never be logger.
logService.log("LeakCanary disabled in release build")
}
}
}

As “chucker” and “leakCanary” dependencies are available only for “debug” source set these cannot be initialised for “release” build variants. Thus “initializeStetho()” and “initializeLeakCanary()” methods are left blank and act as a placeholder methods. These placeholder methods are very much required to be defined inside “release/” source set else the code wont compile.

Configuring source set based on “Product Flavours”

Similar to configuring source sets based on “buildTypes”, we can create source sets based on “productFlavors”. Having a source set specific to product flavors allows Gradle to package source code and resources required only by a particular flavor apart from the “main/” source set which is shared across all the build variants. Below is how we can declare dependencies available only for selective product flavors:

Declaring flavored dependencies

Here we have declared “Firebase In-App Messaging” as a flavored dependency (replace <productFlavor> with the actual flavor defined in your Gradle file). With this Firebase in-app messaging dependency will not be shipped as part of the “main/” source set and can only be accessible for the product flavor it has been defined. Below is the package structure to have code accessing the dependency:

A typical package structure to define the source set for a product flavor
  1. AndroidManifest.xml: This is the main Android Manifest that defines all the activities, services, and permissions that are used across the project. The <application> element defined refers to BaseApplication.kt
  2. BaseApplication.kt: This is the base application file that overrides onCreate() and consists of all application-level stuff that is common across the app for all the build variants.
  3. ProductFlavorApplication.kt: This is the application file that is specific to product flavor, extends the BaseApplication.kt, and consists of code only relevant for the build variant referring to product flavor.
  4. AndroidManifest.xml: This is specific to only the defined product flavor and defines <application> element which refers to ProductFlavorApplication.kt (instead of BaseApplication.kt). Make sure to add the “tools:replace” attribute with value as “android:name”.This ensures the “mergedManifest” finally compiled by Gradle refers to the application class placed inside “productFlavor/” source set instead of the one placed in “main/” source set
AndroidManifest.xml for <productFlavor> source set

5. firebaseIn-AppMessaging: All the classes or methods that depend directly or indirectly on the “firebase-inappmessaging-display” dependency should go here. All these classes have to be deployed inside the “<productFlavor>” source set else the code won't compile as it won't be able to resolve dependencies.

Class diagram

Class diagram for configuring flavored dependency

That's it!! and we are done configuring the source sets based on build types and product flavors. Now we can configure any feature set, app behavior, or app branding by defining different source sets, and that too without losing code readability and flexibility.

Summary

With this, we end the two-part series on “Scaling Android Apps with Gradle”. We learned how Gradle can act as a silver bullet in configuring android apps without adding any code overhead or maintenance havoc. At OLX, we focus on solving business problems through technology and ensuring our android apps remain flexible and scalable enough to cater to the ever-growing business needs.

Hope you enjoyed reading it and found it helpful ✌️.

In case of any feedback or queries please post it in the comment section or reach me directly on LinkedIn or by Email. At last, if you found the blog useful don’t forget to hit the clap icon :)

At last, we are hiring at multiple levels for our Android team. Join and be a part of incredible growth. Visit our OLX careers page to view all the open positions. Wish you luck :)

--

--

Technical Architect | Android Chapter Lead | Domain Expert @OLX Group.