White-labeled applications — The untold story of android “Product Flavors”
Working in a product company I came across a problem of making white-labelled applications for our clients. So like any naive developer, I started cloning the main project and making multiple copies of my code to give new clients their custom branded application.
But this approach had issues, launching a new feature and maintaining sync between all the projects started becoming a serious and often times more time consuming than building the feature itself. And the bigger nightmare was fixing bugs and then replicating the same fix in all the projects.
As the problem become severe and serious, I started searching for solutions and after a good amount of research I had 2 possible solutions :
- Creating an internal library of all the common code
- Product Flavors
First solution though easier to implement, but it was not the perfect solution at scale. Releasing and updating the library version in all the projects at scale for even a minor change can’t be the best solution to this problem.
Second, and the lesser known solution was using Gradle Flavors to make multiple variants of a single code base, with different resource files that are complied as needed in while building the project.
The solution sounded like a dream in theory, but implementation was a challenge. Lack of proper documentation and elaborate description made it really difficult to implement in a production stage application.
The official google documentation gives a sample hello world project that is barely enough to understand the working of this amazingly powerful feature provided in the Core Android SDK.
About Product Flavors
Build Variants enable you to create different application versions and manage each version using the same base source code. This allows you to customize your application to different targets, requirements, or usage scenarios. These versions are then compiled and used for distribution.
android {
defaultConfig {
applicationId "com.example.myapp"
}
productFlavors {
free {
applicationIdSuffix ".free"
}
pro {
applicationIdSuffix ".pro"
}
}
}
The official android documentation explains product flavors as a way to make multiple variants of an application such as “Demo”, “Paid”, “Free” etc. But implementing it in a white-labelling scenario is a little complex than that. Choosing the correct structure for the project that is scalable and efficient was the biggest challenge in implementing product flavors in my application.
The project structure after sycing gradle file looks something like this :
The build creates separate directories in the app>src path for each flavor. The working directory can be selected from the build variants pane in android studio. The same is highlighted with green dot. Only the active directories are complied when you build the project.
Understanding it with an example :
Suppose you have 2 product flavors say client1 and client2, having 1 Kotlin file each say Client1Activity.kt and Client2Activity.kt
While the main directory (or the common part) has a file called MainActivity.kt
So, when we select client1 in build variants pane, only MainActivity and Client1Activity files will be complied by the complier. The compiler won’t know about existence of Client2Activity file while building client1 build variant. Thus removing all the unnecessary code from this flavor’s build and reducing overall size and time of app building.
This opens a lot of doors for the white-labelling use case. Instead of cloning my main project and then changing app colors, logo and name, I can just create a new flavor for each new application and save their color and image drawable resources in the respective directories under app>src in the project structure. With one condition that the name of the resources should exactly match in all the flavors to be able to use those resources in the common code.
So, each flavor can have a string.xml file with a string resource called app_name that defines name of that application. Each flavor can have a drawable file logo.png that can be used directly anywhere in the common or flavor specific code and give desired results each time.
The perfect solution….or is it?
Though the above solution solves a lot of issues, but it has it all problems. In fact the biggest power of this feature turned out to be an issue in my use case. The application only complies the active directories while building. That means, I cannot call any flavor specific code, file or function directly from the common code. Which is, the basic requirement of while-labelling. So if client1 wants to open Client1Activity from a button tap on MainActivity and client2 wants to open Client2Activity, all i would get is compile time error resulting in a failed build.
So after research, I found a third party library written in pure kotlin that gives product flavors a new capability. The library is called Flavor Alias and does exactly what the name suggests.
This library provides the capability to create classes in different flavors that all share a common alias name.
Understanding it by the same example, Say Client1Utils and Client2Utils has the same alias name “ClientUtils” (defined as per library documentation) and a class “ClientCommonUtils” is present in the common code with the same alias name. This creates an alias class while building the application. Which means, any function of ClientUtils class can be called from the common code without any compile time errors.
And the most beautiful part, there is no redundancy of code. Now, if i need to implement a specific function in only client2 flavor, I don’t have to write it in all the flavors and still call it from common code, because in case the compiler doesn’t find the alias class in the flavor code, it’ll automatically fallback to the default functionality that is defined in “ClientCommonUtils”.
This approach, along with the power of product flavors provides a flexible, scalable and efficient way or making white-labelled applications in my use case.
I have been using this approach since October 2020, and the number of applications that I was able to develop and maintain alone with a single code base is now to 50 and growing.