fabernovel loader

Dec 2, 2015 | 5 min read

Tech

Gradle, the Applidium way

Roland Borgese

Developer


FABERNOVEL TECHNOLOGIES
Let’s continue our overview of the Android ecosystem by talking about Gradle.

Gradle is a build automation system included in Android Studio that is used, with the Groovy syntax, to manage and build Android projects. Well used, it enables to finely handle the different build steps and to simplify the continuous integrations. Keep reading for some useful tips on Gradle.

The dependency, mother of all dependencies

To begin, here is an essential tool to make the handling of your dependencies easier: SDK Manager Plugin. It automatically downloads and updates your dependencies (API, support library, emulator). It’s really useful, especially with shared code: once an external project has been downloaded, you have nothing to do. Open the project, wait for the dependencies being downloaded or updated and then you can launch the app!

To enjoy this plug-in, just add this line in the dependencies dictionary in build.gradle at the root of your project:

dependencies {
    classpath 'com.android.tools.build:gradle:1.3.0'
    classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0' //add me here
}

More information about the project on github.

Variants opportunities

productFlavor and buildConfigField

Another useful Gradle feature is productFlavor. Reminder: you can define different productFlavors in your build.gradle which will generate different versions of your application.

One may want several targets for different branches of an enterprise for instance:

productFlavors {
    branchOne {
        applicationId "com.example.branchOne"
        buildConfigField "String", "CONFIG_ENDPOINT", "http://branchOne.com/android"
   }
    branchTwo {
        applicationId "com.example.branchTwo"
        buildConfigField "String", "CONFIG_ENDPOINT", "http://branchTwo.org"
   }
}

You can define buildConfigFields inside these variants. Thanks to those fields, you can gather your entire configuration in a unique file. Use them for constants that must be easily reachable (and not split among several xml files). We use them to define the root url for our HTTP requests for instance.
You can use them into the code like this: BuildConfig.FIELD_NAME.

A useful tip: you can add dependencies only for some variants. Just prefix the word compile with the variant name:

dependencies {
    compile 'com.android.support:support-v4:22.2.0'
    branchOneCompile 'com.android.support:appcompat-v7:22.2.0'
}

buildType

Alongside productFlavors, there are also buildTypes (debug and release by default). They both are ways to generate variants for your application, merging together in a buildVariant. branchOne and debug will generate the variant branchOneDebug.
The difference is that changing the buildType doesn’t change the code of your application, it’s only differently processed. You can access to more technical details (build optimization, log level, etc.) but the content is the same. On the contrary, changing the productFlavor enables you to change the configuration and so the content of the application.

flavorDimension

If we want to go further and create different targets in accordance with different criteria, we can use the flavorDimensions. They make it possible to generate variants according to several dimensions. For example this code:

flavorDimensions "brand", "environment"
productFlavors {
    branchOne {
        flavorDimension "brand"
        applicationId "com.example.branchOne"
        buildConfigField "String", "CONFIG_ENDPOINT", "http://branchOne.com/android"
    }
    branchTwo {
        flavorDimension "brand"
        applicationId "com.example.branchTwo"
        buildConfigField "String", "CONFIG_ENDPOINT", "http://branchTwo.org"
    }
    stubs {
        flavorDimension "environment"
    }
    preprod {
        flavorDimension "environment"
    }
    distrib {
        flavorDimension "environment"
    }
}

creates the variants:

By modifying the applicationId as above, different versions of the app can coexist in your phone.

A dynamic Manifest

You can insert placeholders in your Manifest thanks to the syntax ${name}. Here an example:

<activity android:name=".Main">
    <intent-filter>
        <action android:name="${applicationId}.foo">
        </action>
    </intent-filter>
</activity>

With this piece of code, the ${applicationId} placeholder will be replaced by the real applicationId value. For the branchOne variant for instance, it will become:

<action android:name="com.example.branchOne.foo">

It could be useful during push subscription in particular, because we need to populate the Manifest with a different applicationId according to the variant.

If you ever need to create your own placeholders, you can define them in the manifestPlaceholders. The syntax is:

productFlavors {
    branchOne {
        manifestPlaceholders = [branchCustoName :"defaultName"]
    }
    branchTwo {
        manifestPlaceholders = [branchCustoName :"otherName"]
    }
}

To go further

If you need to do more difficult things, you can execute additional code in the task applicationVariants.all.
Considering, for instance, I need to have a specific applicationId for the branchTwo and distrib variant, I can write in the build.gradle file :

applicationVariants.all { variant ->
    def mergedFlavor = variant.mergedFlavor
    switch (variant.flavorName) {
        case "brancheTwoDistrib":
            mergedFlavor.setApplicationId("com.example.oldNameBranchTwo")
            break
    }
}

Sometimes buildTypes – flavor combinations make no sense and we would like to tell Gradle not to generate these variants. It’s not a problem, just do this using a variant filter, like this:

variantFilter { variant ->
    if (variant.buildType.name.equals('release')) {
        variant.setIgnore(!variant.getFlavors().get(1).name.equals('distrib'));
    }
    if (variant.buildType.name.equals('debug')) {
        variant.setIgnore(variant.getFlavors().get(1).name.equals('distrib'));
    }
}

In this example, we tell Gradle that variants with a debug buildType mustn’t generate the distrib flavor whereas release ones must only generate the distrib flavor.

Facilitate continuous integration

The continuous integration is a major challenge at Applidium. It’s a way to guarantee your code quality during the various steps of development. Here are some tips we use to make its setup easier.

Thanks to buildVariants

The buildVariants (buildType + productFlavors) we have seen above allow the existence of several development environments with their own resources and configuration. With different development environments, you can easily switch between fake data (stubs, suitable for tests) or an environment closer to real use conditions (preprod).

With a plug-in

classpath 'com.novoda:gradle-android-command-plugin:1.4.0'

This plug-in enables to execute adb commands into the Gradle tasks. Indeed, it’s important to concentrate all the deployment steps in one tool. Thus, all the deployment chain can be called from a single command and chances of an error are minimized. When we build+run an application with Android Studio, Android Studio hides the fact that it uses several tools: it calls Gradle assemble task then installs and launches the app via adb. On a CI server or another place where you don’t have Android Studio, you can build and run an app thanks to this plug-in.

Add this line in the dependencies of the root build.gradle, just like you would do with SDK Manager:

dependencies {
  classpath 'com.android.tools.build:gradle:1.3.0'
  classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
  classpath 'com.novoda:gradle-android-command-plugin:1.4.0'  //add me here
}

Automatically increase the versionCode

In order to easily detect problems, it is recommended to increase the version code at each deployment. To avoid mishaps, you can create a Gradle task that increases the version code by one.
Here is the code, to be added in build.gradle:

void bumpVersionCodeInFile(File file) {
    def text = file.text
    def matcher = (text =~ /versionCode ([0-9]+)/)
    if (matcher.size() != 1 || !matcher.hasGroup()) {
        throw new GradleException("Could not find versionCode in app/build.gradle")
    }
    def String versionCodeStr = matcher[0][1]
    def versionCode = Integer.valueOf(versionCodeStr)
    def newVersionCode = versionCode + 1
    def newContent = matcher.replaceFirst("versionCode " + newVersionCode)
    file.write(newContent)
}
task(bumpVersionCode) << {
    def appGradleFile = file('app/build.gradle')
    if (appGradleFile.canRead()) {
        bumpVersionCodeInFile(appGradleFile)
    } else {
        throw new GradleException("Could not read app/build.gradle");
    }
    def wearGradleFile = file('wear/build.gradle')
    if (wearGradleFile.canRead()) {
        bumpVersionCodeInFile(wearGradleFile)
    }
    // No exception here since projects are not required to have a wearable app
}

Tell us your interests! Share with us your ideas or questions about Android and we’ll do our best to help you!

Contact @Applidium
logo business unit

FABERNOVEL TECHNOLOGIES

150 talents to face technological challenges of digital transformation

next read