Add some flavor to your application product flavors configuration cover

In the course of writing Android application, it often happens that we have to build more than one application from the same code base. During the development process, these are the most common versions supported in different environments - test and production. In addition, we sometimes need a free and paid version - another 2 versions of the application. I have recently met the issue of creating another instance of an existing application that covers nearly 95% of the functionality. Creating a completely new project on the basis of the previous one would not be a good solution because some of the modifications or improvements would have to be done twice. You can do it differently - you can use product flavors. In this post, I would like to show you how to efficiently create multiple flavors in different dimensions as well as manage source sets efficiently. 

Build variants, types, flavors...

Build Variants - simply said - are versions of your application. Although you don't create build variants directly - you specify build type rules as well as product flavors. These rules and scripts are used to generate all the build variants. 

Build types (debug/release) apply build and packaging settings - debug options, ProGuard configuration, signing config etc.

Product flavors, however, are responsible for features and device requirements, like choosing the right resource or source code set or setting minimum API levels.  

Example

Since we are talking about flavors all the time, our example will concern thing that has them all - ice cream! Let's assume you have generic ice cream. The process of making chocolate and vanilla ice cream is pretty much the same - it differs only in one small bit at the end. The (code)base for all ice cream is nearly identical. 

Flavor dimensions

Let us create our flavors. We would like to have chocolate and vanilla ice cream with an option to be served either in cone or bowl. So we should create 4 different flavors. 

  productFlavors {
        chocolate {

        }
        vanilla {

        }
        cone {

        }
        bowl {

        }
    }

When we sync up our Gradle, the problem is visible at first glance. The script didn’t take into account that bowl/cone is a different type of difference than chocolate/vanilla. 

How do we fix that? We introduce flavor dimensions 


    flavorDimensions "taste", "serving"

    productFlavors {
        chocolate {
            dimension = "taste"
        }
        vanilla {
            dimension = "taste"
        }
        cone {
            dimension = "serving"
        }
        bowl {
            dimension = "serving"
        }
    }

Flavor dimensions tell our Gradle build which flavor is responsible for given area of variation. In that particular case - the flavor of ice cream (which will be called “taste” to avoid confusion) and method of serving. Now, Gradle knows what we mean and creates build variants according to new rules - [chocolate/vanilla] + [cone/bowl] + [debug/release].

If we decide that we do not want to use specified build variant, we can turn it off or ignore it. We do that by properly setting the variant filter under android tag. Let's say we do not want to create chocolate ice cream in a cone, but only when in debug build type. 

variantFilter { variant ->
        def names = variant.flavors*.name
        if (names.contains("chocolate") && names.contains("cone") && variant.buildType.name == "debug") {
            setIgnore(true)
        }
    }

Going further, we can differentiate the builds by adding buildConfigField to defaultConfig and then overwriting it when needed. For instance, all of the ice cream are not coated and do not have sprinkles by default, but we would like chocolate ice cream served in a bowl to have both coating and sprinkles, and vanilla ice cream to have a coating but only when served in a cone.

defaultConfig {
        (...)
        buildConfigField "boolean", "addSprinkles", "false"
        buildConfigField "boolean", "isCoated", "false"
    }
buildTypes {
        release {
           (...)
        }
        applicationVariants.all { variant ->
            def names = variant.productFlavors*.name
            if (names.contains("chocolate") && names.contains("bowl")) {
                variant.buildConfigField "boolean", "isCoated", "true"
                variant.buildConfigField "boolean", "addSprinkles", "true"
            }
            if (names.contains("vanilla") && names.contains("cone")) {
                variant.buildConfigField "boolean", "isCoated", "true"
            }
        }

    }

You can later use those variables directly in your code like so: 

        if (BuildConfig.addSprinkles){
            //add some sprinkles!
        }
        if (BuildConfig.isCoated){
            //ice cream is coated
        }

The real power of production flavors, however, is differentiating resource and source code sets depending on build variant currently being built. By default, all resources and source files are taken from the main folder, but if we create new source folder with the corresponding name, resources will be overwritten by their copies in flavor folders - if present. To create a resource folder, eg layout folder for our vanilla build we just go to File -> New -> Resource Directory, choose source set and Android Studio will take care of the folder structure for us. 

Every layout resource you will put in here will overwrite layout resources from the main source set while building any build with vanilla flavor.

Overriding classes

While talking about overriding layouts, I have to mention one common issue you may come across. Different elements in differents source sets. Let's consider drawer menu. Normally you put every menu element into <item> tag in the layout XML file, and then you handle menu clicks using switch concerning these items ids. If you will, however, have a different number of elements, thus different ids, you will not be able to reference vanilla specific menu item in the chocolate flavor. You would run into no resource found error at compile time. One solution would be to create one big menu XML with all the items and toggle their visibility, but that will create a great mess in the long run. The problem is you cannot overwrite class from the main java source set because it throws a duplicate class exception.

My solution to this problem is : 

  1. Move all of your non-flavor-specific functionalities to base or common version of your class.
  2. Make your class extend the base class.
  3. Create a version of your class for every flavor, overwriting the functionalities in which they differ, leaving the common part in the base class.
  4. Create flavor-specific java folder using File -> New -> Folder -> Java Folder
  5. Put your flavor-specific class in that folder.

If you have multiple flavors, and some of them will use the same version of one class, you can add a common source set to you flavor in gradle file by specifying the source set to add: 

 sourceSets {
        flavorName.java.srcDir 'src/sourceSetName/java'
    }

 

Using this method you can create for instance 2 versions of the same Activity, having completely different logic without the need of modifying the flow of your application that much.

Summary

Using these couple tips and tricks can make creating multiple apps from single code base simple and painless. Product flavors can save you tons of lines of code and therefore - time. Overriding resource and source set is very effective in the long run. Using this approach, the crucial part is to sit down and think about common parts and design your classes properly even before you start writing. Do it right, follow the guidelines above and generating multiple build variants will be effortless.

Post tags:

Join our awesome team
Check offers

Work
with us

Tell us about your idea
and we will find a way
to make it happen.

Get estimate