Playing with Gradle[3]: Flavors, BuildTypes and Variants

Playing with Gradle:

Chapter 1: Basics

Chapter 2: Android Tasks Basics

Chapter 3: Flavors, BuildTypes and Variants

Chapter 3 1/4: Flavors And Manifest

Chapter 3 and some few: Understanding the life cycle.

Chapter 3 and some few more: Code Coverage on Android with Jacoco

Chapter 4: Getting real

(Download the ebook: https://android2ee.com/Tutoriaux/Playing-with-Gradle.html for free and without any registration or what ever, it’s a gift to the community).

Let’s keep discovering Gradle and now it’s time to talk about flavors, build types and variants. Yes. All the discussion here belongs to your module (not the project) build.gradle.

Flavors is the ability to overwrite files at compilation time. We have a lot of tricks to adapt our code behavior depending on context (connectivity, battery, lowram device, gingerbread) using interfaces and factories. But until now, it was not possible to easily build several flavors of a same product and so adapt our product at compilation time.

And this magi is Flavors. But let’s begin by the beginning and let’s have a look at the build type notion.

1 BuildTypes

By default there are two build types: the one for debug and the one for production.

It’s not compile time adaptation because you don’t release to the public a debug version. Build type are really part of the process development more than a customization of your application.

We generally have this bloc in our android block (of our build.gradle):

signingConfigs {
    release {
        storeFile file("javaKeyStore.jks")
        storePassword "passwordOfTheJks"
        keyAlias "keyName"
        keyPassword "passwordOfTheKey"
    }
}
buildTypes {
    release {
        minifyEnabled true
        shrinkResources true
        signingConfig signingConfigs.release
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    debug {
        applicationIdSuffix '.debug'
        versionNameSuffix '.debug'
    }
}

This is the most generic buildTypes you can have.

When we look at the release block, we have the signingConfig to sign the apk for the release. When have enabled shrink and minify to remove the unnecessary code and resources. We run proguard to obfuscate our code.

A word about proguard: It’sa pain, especially when you have lirbaies in your project. A trick: Make often release build to see if something have changed don’t wait the day before the release to production, you’ll regret.

When we look at the debug blog, we have the suffix added. The one for the application is the most important, it give us the ability to deploy on the same device the both version (it’s related to the unicity constraints on {applicationId,signatureKey}.

We’ll see in another chapter how to extract your password and id into properties files, avoiding having them in the git repo or in the project (security reasons).

2 Flavors

Let’s start with an example. I made a quick application for my son to learn multiplication tables.

And I said to myself, why don’t I do one for my daughter too ? One for multiplication, the other for addition with specific branding for each of them.

But I don’t want to copy paste the project and start a second one. Because I will lose all the improvements done on the other project. Complex branching system using git… No, too hazardous and full of merges. I could also have make a core library, but what a mess for a simple request:

I just want to customize pieces of my code, living the architecture at the center and customize the features according to a specific branding.

The answer is flavors.

When you compose buildTypes and flavors, it gives you build variants.

2.1 Basics

So I create it:

productFlavors{
 basile{
 }
 lila{
 applicationId “com.android2ee.lila.multiplication”
 
}
 }

And then, I run gradlew sourceSets and compare with what it was before. I remove content and keep only head chapter to gain in visibility:

When I run sourceSets without flavor:

androidTest

debug

main

release

test

testDebug

testRelease

When I run sourceSets after creating the flavors:

androidTest

androidTestBasile

androidTestLila

basile

basileDebug

basileRelease

debug

lila

lilaDebug

lilaRelease

main

release

test

testBasile

testBasileDebug

testBasileRelease

testDebug

testLila

testLilaDebug

testLilaRelease

testRelease

And yes, sourceSets shows you for each variant where are the information of your project. What is the task for compiling this variant, where are the sources (java,manifest,res,assets,…).

But another vision is sourceSets lists all the subfolders your can create in your application module to overwrite specific elements depending on a variants (flavor:Basile, build type:Release) and tests.

And that all. You want to overwrite a specific file, for exemple ic_launcher, because in Debug for Lila you want it with a smile, just define it under :

lilaDebug/res/mipmap-***/ic_launcher

If you want a specific for all lila variants, just do it in lila folder, and it’s done:

I mean, it’s done:

It works the same for all the resources and Java code. You just need to overwrite the file defined in main by your in the variants you want. It’s based on the path of the file.

So flavors are ordered and it’s important because it will defined how files are overwrited. For example, in BasileMultiplicationDebug, if the same element is defined in Basile and Multiplication, the one of Basile will be the one that stays, overwriting the one of multiplication. We’ll see the flavor Multiplication latter).

The other important information to have here is:

Flavor’s resources can overwrite main’s resources

Flavors’ Java code CAN NOT overwrite those of main.

Most of time, on the project, you have mocked and prod flavors, where

· mocked is a flavor for test where all your services/dao/communication is mocked and dedicated to unit test/ integration tests

· prod is the flavor where you have your real application’s bricks bound together

They belong to the same dimension “TestContext” or something like that.

2.2 Dimensions

Then I though, ok, but Lila won’t learn multiplication, she is too young, let’s start with additions…. But wait, réfléchissement jean-pierre, one day, she will also make multiplications, so do I create two flavors instead of only one : lilaAddition and lilaMultiplication.

Yes, but no, I will think in terms of dimension :

[Making a table on Medium is a nightmare]

________|____Addition________|____Multiplication

Lila_____|___LilaAddition______|___LilaMultiplication

Basile___|__BasileAddition_____|____BasileMultiplication

So I will say, I have two types of flavor, one is the kids and the other is operation. And if I could easily explain that basile doesn’t need his addition dimension, it will be cool.

So let’s go:

android {
    ...
//Give a name to your dimension
flavorDimensions "enfants", "operator"
//define your flavors (as one flavor has a dimension they must all have one)
productFlavors{
    basile{
        dimension "enfants"
    }
    lila{
        dimension "enfants"
        applicationId "com.android2ee.lila.multiplication"
    }
    multiplication{
        dimension "operator"
    }
    addition{
        dimension "operator"
    }
}
//Remove the BasileAddition and LilaMultiplication flavor
android.variantFilter { variant ->
    if(variant.getFlavors().get(0).name.equals('basile')
            && variant.getFlavors().get(1).name.equals('addition')) {
        variant.setIgnore(true);
    }
    if(variant.getFlavors().get(0).name.equals('lila')
            && variant.getFlavors().get(1).name.equals('multiplication')) {
        variant.setIgnore(true);
    }
}
}

Remember I have declared kids first, so the code and resources of the kids flavor will always overwrite those from operator and main.

In your flavor description you can define the following attributes:

· applicationId

· minSdkVersion

· targetSdkVersion

· versionCode

· versionName

· signingConfig

The last block is a little bit amazing, but we’ll see that in another chapter more focus on task. The idea is to browse your flavors list and filter it based on flavor’s name.

And I was also able to say, I will do it latter the LilaMultiplication one.

So when I run sourceSets, what happens ?

addition

androidTest

androidTestAddition

androidTestBasile

androidTestBasileMultiplication

androidTestLila

androidTestLilaAddition

androidTestMultiplication

basile

basileMultiplication

basileMultiplicationDebug

basileMultiplicationRelease

debug

lila

lilaAddition

lilaAdditionDebug

lilaAdditionRelease

main

multiplication

release

test

testAddition

testBasile

testBasileMultiplication

testBasileMultiplicationDebug

testBasileMultiplicationRelease

testDebug

testLila

testLilaAddition

testLilaAdditionDebug

testLilaAdditionRelease

testMultiplication

testRelease

Et bien, ça en fait du monde. A lot of variants.

But at the ends, with only few changes and some copy/paste (yes, to create the flavor at first, copy/paste the folders from main to your flavor using the explorer, not in AS, to have the folders’ structure), I achieve that:

And using AS, it’s really easy, you have the build variants at the bottom right to help choose which variant you want to deploy:

3 Step by step example

3.1 Gradle file

Let’s say I did my build.gradle file like explained:

android {
    ...
//Give a name to your dimension
flavorDimensions "enfants", "operator"
//define your flavors (as one flavor has a dimension they must all have one)
productFlavors{
    basile{
        dimension "enfants"
    }
    lila{
        dimension "enfants"
        applicationId "com.android2ee.lila.multiplication"
    }
    multiplication{
        dimension "operator"
    }
    addition{
        dimension "operator"
    }
}
//Remove the BasileAddition and LilaMultiplication flavor
android.variantFilter { variant ->
    if(variant.getFlavors().get(0).name.equals('basile')
            && variant.getFlavors().get(1).name.equals('addition')) {
        variant.setIgnore(true);
    }
    if(variant.getFlavors().get(0).name.equals('lila')
            && variant.getFlavors().get(1).name.equals('multiplication')) {
        variant.setIgnore(true);
    }
}
}

Note that with this gradle configuration I can deploy on the same device, Lila and Basile application because I change the applicationId for the Lila flavor.

Now, using AndroidStudio, build variants bar, I just have to select the build I want to deploy on the phone and click on the green arrow, like usual.

3.1.1 Cutsom task: print variants name

If I want to make a task that write the name of all the variants of the project. I have to define it in my build.gradle (always the same), like this :

task printVariantsName(){
    doLast{
        android.applicationVariants.all{ variant ->
            println variant.name
        }
    }
}

So this code defined a task name printVariantsName and we that launched, when it has finished the “job”, an action (real word is closure). This action just ask the android plugin to give us all its variants (android.applicationVariants.all ). We take each variant one by one and print its name (variant -> println variant.name).

If I run it I have the following outputs:

lilaAdditionDebug

lilaAdditionRelease

basileMultiplicationDebug

basileMultiplicationRelease

Damned it, all this job to have the same result than in AndroidStudio with one click on a bar:)

3.2 Setting your folders structure

Ok, from my point of view, the best way and quickest way to create your folders structure is to use the files explorer or your system.

You first create your flavors folder. Then you copy paste src and res from main to all the flavors you created. And then delete all the files that are not folders in your flavors (be smart do it at the first paste…).

You’re structure is finished you can go to work.

3.3 Customizing Resources

I want to customize strings, colors and icon launcher picture. All those elements are resources, so I have to copy paste them in lila and basile and adapt them as desired.

As I have no customization related to tests or instrumentation tests, I don’t have to create more folders than my flavors declared. But if you want to customize a tests context, you can do it by creating the associated variant folder (like when running sourceSets).

A really good practice is to overwrite (copy files) only the resources you overwrite (really change). It means, don’t copy by default all the files from main to yours flavors, do it only for files you change. I spent a quarter, adding a switch in a layout with no result because I had copied/pasted this layer also in the basile’s flavor without paying attention.

3.3.1 Drawables customization

Let’s begin by changing the icon of the application. For that the only stuff to do is to overwrite the ic_launcher (*dpi) in Lila and Basile flavors. To do that, just use the Android Studio Image creation wizard and create your ic_launcher for each flavor.

The last screen of the wizard with the button finish has a combo at its top where you define the flavor for this picture. Exactly what we need:

So you just have to create your mipmap and it’s done.

This is the result in my folder’s structure:

3.3.2 String Cutomization

For the string, I did the same, but this time I copied/pasted the files instead of using a wizard and I delete the strings I didn’t change:

lila/res/values/string.xml

<resources>
    <string name="app_name">Addition Lila</string>
    <string name="ass_act_toolbar_title">Lila apprend la table de %1$d</string>
    <string name="ass_act_toolbar_subtitle">Elle est trop forte Lila</string>
    <string name="question_string">%1$d + %2$d = %3$d</string>
    <string name="question_string_init">%1$d + %2$d = ? </string>
</resources>

basile/res/values/string.xml

<resources>
    <string name="app_name">MultiplicationBasile</string>
    <string name="ass_act_toolbar_title">Basile apprend la table de %1$d</string>
    <string name="ass_act_toolbar_subtitle">Il est trop fort basile</string>
    <string name="question_string">%1$d x %2$d = %3$d</string>
    <string name="question_string_init">%1$d x %2$d = ? </string>
</resources>

main/res/values/string.xml

<resources>
    <string name="app_name">Default Title</string>
    <string name="ass_act_toolbar_title">Basile apprend la table de %1$d</string>
    <string name="ass_act_toolbar_subtitle">Il est trop fort basile</string>
    <string name="question_string">%1$d x %2$d = %3$d</string>
    <string name="question_string_init">%1$d x %2$d = ? </string>
    <string name="mainact_max_multiplication_value">Quelle est la table que tu souhaites apprendre ?</string>
    <string name="mainact_maxvalue_edt_hint">Tape un nombre</string>
    <string name="mainact_txvTemps">Temps : %1$ds</string>
    <string name="mainact_txvScore">%1$d pts</string>
    <string name="mainact_txvQuestionNumber">%1$d/%2$d</string>
    <string name="mainact_answer_edt_hint"> \? </string>
    <string name="mainact_start">Start</string>
    <string name="mainact_switch_positive">Oui</string>
    <string name="mainact_switch_negative">Non</string>
    <string name="mainact_switch_question">Uniquement cette table</string>
</resources>

The important element to pay attention here is the string.xml of lila and basile flavors contain only strings that need to be adapted to the flavor. You don’t have to copy all default strings. It’s exactly the same with you res folder drawable-hdpi/-mdpi-ldpi… The system merge the resources in a natural way. With flavor, always take attention to duplication and avoid it if unnecessary. Because,

With flavors when mess comes, mess is huge.

You can or not delete the string in main/string.xml already defined in all the flavors, but it’s not as obvious as it looks first that the good practice. That’s why I let them in my string.xml file. I prefer string error than NPE and I am not sure that this string has to be translated for all the flavors.

3.3.3 Colors customization

Then I want to specify colors to use for each flavor. So I copy my main\res\values\colors.xml to lila\res\values\colors.xml and basile\res\values\colors.xml. I change the values I want, especially the color Primary, PrimaryDark and Accent, because I rely on the support library.

lila\res\values\colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#fc2cbe</color>
    <color name="colorPrimaryDark">#cc0c93</color>
    <color name="colorAccent">#40ff70</color>
    <color name="shape_default_stroke">#d4d4d4</color>
    <color name="shape_default_solid">#d4d4d4</color>
    <color name="item_background_translucent">#999e9c9c</color>
    <color name="item_background_opaque">#5fd5def2</color>
</resources>

basile\res\values\colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#0db656</color>
    <color name="shape_default_stroke">#d4d4d4</color>
    <color name="shape_default_solid">#d4d4d4</color>
    <color name="item_background_translucent">#999e9c9c</color>
    <color name="item_background_opaque">#5fd5def2</color>
</resources>

3.3.4 Resources Redirection

Sometimes, it’s usefull to have some concept on resources redirection. The typical example is you use a resource A which uses a resource D, but D is also use for another resource C and you want to customize D within A but not within C. For example, D is a drawable, A is a layout (D is used as background) and C is an ImageView (and D is used as android:src or through code with setImageResource).

The simple way is to duplicate D and create DWithinA and DWithinC and customize DWithinA in your flavors. But, if D is drawable, you increase the size of your apk and the number of files in your project which is not a good idea.

Let’s assume, D is a drawable. I make a redirection to it, I create a file called resources_flavor_redirection.xml in main:

And add the redirection inside:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <drawable name="main_act_keyboard_background">@mipmap/ic_drhulk</drawable>
</resources>

Then in A, which is in the example, the “background” of a layout in my main_activity.xml layout:

...<FrameLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:foregroundGravity="center">
    <ImageView
        android:layout_width="285dp"
        android:layout_height="285dp"
        android:src="@drawable/main_act_keyboard_background"/>
    <include layout="@layout/keyboard"
        android:visibility="visible" />
</FrameLayout>...

Then I just have to play the flavors game with my resources_flavor_redirection :

lila\res\values\ resources_flavor_redirection.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <drawable name="main_act_keyboard_background">@mipmap/ic_spiderdroid</drawable>
</resources>

basile\res\values\ resources_flavor_redirection.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <drawable name="main_act_keyboard_background">@mipmap/ic_captaindroid</drawable>
</resources>

And for the C component, nothing have changed. I can still use the initial D drawable without risk of missed overwriting issues, like in that line of code:

//then create you Dialog itself
builder.setIcon(R.mipmap.ic_captaindroid)
// Set Dialog Title
.setTitle("Game Over")
// Set Dialog Message
//.setMessage("Alert DialogFragment Tutorial")
//or your specifc view
.setView(view)

3.3.5 Declaring Resources in your gradle’s flavor block

We can define attribute in our flavor block (and in our gradle file by the way).

For example:

defaultConfig {
 resValue “string”, “hidden_string”, “I love you my sweety”
 
}
 //Give a name to your dimension
 
flavorDimensions “enfants”, “operator”
 
//define your flavors

//(as one flavor has a dimension they must all have one)
 
productFlavors{
 basile{
 dimension “enfants”
 
//resValue “boolean”,”basile_dimension”,”true” -> not allowed
 //resValue “int”, “int_allowed”, “1” -> not allowed
 
resValue “string”, “hidden_string”, “I love you my basilou”
 
resValue “color”, “int_allowed”, “#FF00ff”
 
}
 lila{
 dimension “enfants”
 
applicationId “com.android2ee.lila.multiplication”
 
resValue “string”, “ hidden_string “, “I love you my Lila”
 
resValue “color”, “int_allowed”, “#FF00ff”
 
}
 multiplication{
 dimension “operator”
 
}
 addition{
 dimension “operator”
 
}
 }

And you can use them in your code or your build or your manifest.

if(BuildConfig.isallowed){
 Log.e(TAG,”BuldConfig IsAllowed and ResValue “

+getString(R.string.hidden_string));
 }

But I met problem with bool, int and others values, so I am a little disappointed.

3.3.6 Run your project(s)

It’s over, our customization is finished, we can run the project. To do so, select the build you want and run it:

Tu peux aussi frimer en le faisant via gradlew install** si t’es en mode surfeur :)

You obtain:

Both applications are installed and they have their own icon:

The Basile’s application:

And the Lila’s application:

3.4 Customizing Java code

The rule:

Flavor’s resources can overwrite main’s resources

Flavors’ Java code CAN NOT overwrite those of main.

It means, in Java, you can not have a class overwritted for a specific flavor and have its default behavior defined in main. You need to remove the class from main and paste it in all your flavors. This force all others flavors to define this class. And it’s a huge constraint.

So, back to our example, one of my goal was to make a basile application to learn multiplication and a Lila one to make addition.

This is where Java code customization is needed in my project.

To do that, I just extract the operation * (everywhere a multiplication is done bound to the assessment process) from all my code and create a Java class called AssessmentOperation with one method public int calculate(int value, int factor)

That way, I extract the code I want to customize in a single small class. You will always have to refactor your code to make that isolation, else you will have too much code duplication.

So I create, in the flavor addition and multiplication (the ones in the “operator” dimension), my class AssessmentOperation. And I removed it from main:

then I just have to implement my method in each flavor (addition, multiplication):

multiplication\AssesmentOperation

public class AssesmentOperation {
    public static int caculate(int value,int factor){
        return value*factor;
    }
}

addition\AssesmentOperation

public class AssesmentOperation {
    public static int caculate(int value,int factor){
        return value+factor;
    }
}

And it’s done.

In my main flavor, I just call that method like that:

operationFactor[i]= getOperationValue(position,operationTable);
value[i]= getValue(position, operationFactor[i],operationTable);
answer[i]=AssesmentOperation.caculate(value[i],operationFactor[i]);

And here comes the result: