Understanding Gradle for Android

I have always found Gradle to be the necessary evil in Android but there is an article which I found which really widened my sight about what is Gradle and how to use it. Here is the article. Please have a look at it:

Understanding Gradle for Android

Copy/Paste below:

Groovy

Syntax

Gradle files are basically Groovy scripts. Syntax is easy to grasp if you know Java, and for us it’s important that:

  • there’s no need for parentheses when calling methods with at least one parameter (if it’s unambiguous):
def printAge(String name, int age) {
    print("$name is $age years old")
}

def printEmptyLine() {
    println()
}

def callClosure(Closure closure) {
    closure()
}

printAge "John", 24 // Will print "John is 24 years old"
printEmptyLine() // Will, well, print empty line
callClosure { println("From closure") } // Will print "From closure"
  • if the last parameter is a closure (for now think lambda), it can be written outside the parentheses:
def callWithParam(String param, Closure<String> closure) {
    closure(param)
}

callWithParam("param", { println it }) // Will print "param"
callWithParam("param") { println it } // Will print "param"
callWithParam "param", { println it } // Will print "param"
  • If you invoke Groovy method with named parameters, they are converted into a map and passed as first argument of the method. Other (non-named arguments) are then appended to parameters list:
def printPersonInfo(Map<String, Object> person) {
    println("${person.name} is ${person.age} years old")
}

def printJobInfo(Map<String, Object> job, String employeeName) {
    println("$employeeName works as ${job.title} at ${job.company}")
}

printPersonInfo name: "John", age: 24
printJobInfo "John", title: "Android developer", company: "Tooploox"

This will print "John is 24 years old" followed by John works as Android developer at Tooploox. Mind that in both cases the result will be the same regardless of parameters order! Also notice omitted parentheses in both calls.

Closures

One important feature that needs some explaining are closures. If you’re familiar with Kotlin, you might find below explanation somewhat similar to function literals with receiver.

Closures in Groovy can be thought of as lambdas on steroids. They’re blocks of code that can be executed, can have parameters, and return values. What’s different is that we can change the delegate of a closure. Let’s consider following code:

class WriterOne {
    def printText(str) {
        println "Printed in One: $str"
    }
}

class WriterTwo {
    def printText(str) {
        println "Printed in Two: $str"
    }
}

def printClosure = {
    printText "I come from a closure"
}

printClosure.delegate = new WriterOne()
printClosure() // will print "Printed in One: I come from a closure
printClosure.delegate = new WriterTwo()
printClosure() // will print "Printed in Two: I come from a closure

We can see that printClosure calls printText method on the delegate it is provided (the same goes for properties). We will see later why this is crucial in Gradle.

There’s actually a bit more to delegates and how closure’s statements are executed. You can read more about delegation and delegation strategies in Groovy documentation.

Gradle

Script files

There are three main script files that Gradle uses. Each one is a block of code (closure, anyone?) executed against various objects:

  • build scripts in build.gradle files. These are executed against Projectobjects;
  • settings scripts in settings.gradle files, executed against Settingsobject;
  • init scripts used for global configuration (executed against Gradleinstance).

Projects

Gradle build consists of one or more projects, and projects consist of tasks. There is always at least the root project, which may contain subprojects, which in turn can have nested subprojects as well. Common convention is that the root project’s role is only to orchestrate group projects, provide common configuration, plugins classpaths etc.

From now on project will refer to whatever subproject we’re currently interested in, and root project will be used when referring to root project specifically.

Creating Gradle-based Android project

In typical Android project we have the following folder structure:

├── settings.gradle # [1]
├── build.gradle # [2]
├── gradle
│   └── wrapper
└── app
    ├── gradle.properties # [3]
    ├── build.gradle # [4]
    └── src
  1. This is root project’s settings file, executed against its Settings instance
  2. Root project’s build configuration
  3. App project’s properties file, injected into app’s Settings
  4. App project’s build configuration

Let’s go step by step, then.

Creating a Gradle project

Let’s create new folder, say example. If we cd into it and execute gradle projects, we can see that it’s already a Gradle project!

$ gradle projects
:projects
------------------------------------------------------------
Root project
------------------------------------------------------------
Root project 'example'
No sub-projects
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
BUILD SUCCESSFUL
Total time: 0.741 secs

If you don’t have Gradle installed locally, you can install it using macports or homebrew, or download an installer from official webpage. You can also create a new project in Android Studio and remove everything except for Gradle wrapper.

Setting up projects hierarchy

If we want similar structure to a default Android project (empty root project and an app project with our application), we need a settings.gradle file. From the documentation we know that settings.gradle script:

declares the configuration required to instantiate and configure the hierarchy of Project instances which are to participate in a build.

Further we read that we can add projects to the build using void include(String[] projectPaths) method. Let’s add a app subproject then:

$ echo "include ':app'" > settings.gradle
$ gradle projects
# ...
------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'example'
\--- Project ':app'

...

BUILD SUCCESSFUL

Total time: 0.666 secs

Colon (:) is used in Gradle to separate paths to subprojects, what we can see here. That’s why we write :app and not app (although in this case app would work as well)

It’s also good practice to include rootProject.name = <<name>> in settings.gradle file. Without it, root project’s name defaults to the name of the folder in which the project is, which may be different for example on a CI server.

Setting up Android subproject

Now we’d normally set up root project’s build.gradle file, but what do we need to put there? Let’s find out by trying to set up an Android project instead.

From the user guide we know that we need to apply com.android.application plugin to our app project. Let’s have a look at apply method signatures:

void apply(Closure closure)
void apply(Map<String, ?> options)
void apply(Action<? super ObjectConfigurationAction> action)

While the third one is the one that’s important — it uses statically typed API — we usually only use the second one, as it takes advantage of feature that we’ve mentioned before — named parameters are passed to the method as a map. To know what keys (parameters names) we can use, we peek into the documentation:

void apply(Map<String, ?> options)

The following options are available:
from: A script to apply. (…)
plugin: The id or implementation class of the plugin to apply.
to: The target delegate object or objects. (…)

We now know we need to pass our plugin id as plugin parameter. We could write apply(plugin: 'com.android.application'), but we also know we can omit parentheses if the invocation is non-ambiguous, which it is. Let’s add apply plugin: ‘com.android.application’ to app’s build.gradle file, then:

$ echo "apply plugin: 'com.android.application'" > app/build.gradle

What now?

$ gradle app:tasks
FAILURE: Build failed with an exception.

* Where:
Build file '(...)/example/app/build.gradle' line: 1

* What went wrong:
A problem occurred evaluating project ':app'.
> Plugin with id 'com.android.application' not found.

BUILD FAILED

Total time: 0.69 secs

Okay then, there’s no com.android.application plugin defined. Well, we’re not surprised — how would Gradle find Android plugin’s jar file? We can see in the user guide we need to add plugin’s classpath, and the repository in which it can be found.

Currently we can configure this classpath either in app’s or in root project’s build.gradle file, because buildscript closure is executed against ScriptHandler, which subprojects also use. This is not recommended though — all plugin dependencies should be declared at root project’s build.gradle instead. Let’s put buildscript block there, then, and discuss what it does:

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.0-beta2'
    }
}

If we add parentheses in our heads, we see all of these are simple method calls, of which some pass Closure as a parameter. If we then dig into the documentation, we read what objects these closures are executed against. In summary:

  • buildscript(Closure) is called on Project instance, and passed closure is executed against ScriptHandler object
  • repositories(Closure) is called on ScriptHandler instance, while passed closure is executed against RepositoryHandler
  • dependencies(Closure) is also called on ScriptHandler, but its argument is executed against DependencyHandler

Which means that:

  • jcenter() is called within RepositoryHandler
  • classpath(String) is called on DependencyHandler (*)

We only need to know that the first call — buildscript — is executed against Project instance. For the rest, the documentation specifies the delegates explicitly.

(*) If you inspect DependencyHandler code, you’ll notice there’s no classpathmethod. This is a special type of call, which we’ll discuss later on with dependencies.

Configuring Android subproject

If we now try to execute some Gradle task, we’ll be greeted with an error:

$ gradle projects
FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring project ':app'.
> buildToolsVersion is not specified.

Obviously, we haven’t put any Android-related configuration yet, but we can see that Android plugin is now applied correctly! Let’s add some configuration:

android {
   buildToolsVersion "25.0.1"
   compileSdkVersion 25
}

We already see what’s happening — somehow there’s android method added to theProject instance, that delegates whatever closure it’s passed to some object (AppExtension in this case), which has buildToolsVersion and compileSdkVersion methods defined. This way Android plugin receives all configurations passed, including default configuration, flavors etc.

In order to run any tasks now we still need two things — AndroidManifest.xmlfile, and a local.properties file with sdk.dir property (or ANDROID_HOMEenvironment variable) pointing to Android SDK location on our machine.

Extensions

But how did android method suddenly appear in the Project instance, against which our build.gradle is executed? In short, Android plugin registered AppExtension class as an extension with android name. This goes out of scope of this post, but what’s important for us is that Gradle adds configuration closure block for each extension object registered by the plugins.

Dependencies

There’s one last block that’s always there, that haven’t been discussed yet – dependencies. Here’s an example:

dependencies {
    compile 'io.reactivex.rxjava2:rxjava:2.0.4'
    testCompile 'junit:junit:4.12'
    annotationProcessor 'org.parceler:parceler:1.1.6'
}

Why is this block special? Well, if you look into DependencyHandler, to which dependencies method delegates the passed closure, you’ll see there’s no compile method on it, nor testCompile or any of those we usually use. Which makes sense — if we add free flavor, we can write freeCompile 'somelib'DependencyHandler can’t define methods for all possible flavors now, can it? Instead, it uses another feature of Groovy language — methodMissing, which allows for catching calls to undefined methods in runtime (*).

(*) Actually Gradle uses abstraction over methodMissing declared in MethodMixIn, but the effect is mostly the same. Similar mechanism can also be applied to undefined properties.

The relevant fragment of the default dependency handler implementation can be found here, and it does the following:

  • If any undefined method is called with more than 0 arguments, and
  • if there exists configuration(*) with the name of that method, then
  • depending on number of parameters and their type, call doAdd method with relevant parameters.

(*) Each plugin can add configurations to dependencies handler. For example java plugin defines compile, compileClasspath, testCompile and some other configurations, specified here. Android plugin on the other hand adds annotationProcessor configuration, as well as <variant>Compile, <variant>TestCompile etc., based on defined build types and product flavors.

While doAdd method is private, it’s being called by add method which is public. Thus, we could(*) rewrite above dependencies block as:

dependencies {
    add('compile', 'io.reactivex.rxjava2:rxjava:2.0.4')
    add('testCompile', 'junit:junit:4.12')
    add('annotationProcessor', 'org.parceler:parceler:1.1.6')
}

(*) But please, don’t do that.

Flavors, build types, signing configs

Let’s consider this piece of code:

productFlavors {
    prod {

    }

    dev {
        minSdkVersion 21
        multiDexEnabled true
    }
}

What does productFlavors method delegate to? If we look into source code, productFlavors is declared like so:

void productFlavors(Action<? super NamedDomainObjectContainer<ProductFlavorDsl>> action) {
    action.execute(productFlavors)    
}

Action<T> in Gradle world is a closure executed against T

So, here we have some NamedDomainObjectContainer which creates and configures objects of type ProductFlavorDsl and stores them alongside their names.

This container also uses dynamic method dispatch to create an object of a given type (here ProductFlavorDsl) and put it into the container along with its (method) name. So if we call method prod with parameter {}, it’s executed against productFlavors instance, which is NamedDomainObjectContainer. Here’s what happens:

  • NamedDomainObjectContainer captures called method’s name,
  • creates ProductFlavorDsl object,
  • configures it against given closure,
  • stores mapping from method name to newly configured ProductFlavorDsl object

We (and Android plugin) can then retrieve ProductFlavorDsl objects from productFlavors. What’s important, we can access them as properties, so in our case we can write productFlavors.dev, and we’ll retrieve ProductFlavorDsl that we’ve put with dev name. This is why we can write signingConfig signingConfigs.debug for example.

Summary

Gradle files are ubiquitous for Android developers, yet they’re often treated as a necessary evil, or at least as a magic black box that does things. But while there’s lots of conventions when it comes to writing Gradle scripts, and Gradle itself adds some complexity over Groovy language, when we get to know both, Gradle files aren’t that magical. I hope after reading this post and applying some curiosity, even that obscure code pasted from Stack Overflow will start to make sense now!

You may also like...