This is the second article in this MAD skills series. In the previous article you’ve seen the basics of Gradle and how to configure the Android Gradle Plugin. In this article you’ll learn how to extend your build by writing your own plugin. If you prefer to watch this content instead of reading, check out the video below:

Starting with version 7.0, Android Gradle Plugin now offers stable extension points for manipulating variant configuration and the produced build artifacts. Some parts of the API were finalized only recently, so I’m going to use the 7.1 version (in Beta at the time of writing) of AGP throughout this article.

Gradle Tasks

I’ll start with a new clean project. If you want to follow along, you can create a new project by selecting the basic activity template.

Let’s start with creating a task and printing out, guess what, hello world. To do this, in the app level  file, I’ll register a new task and name this task ‘hello’.

Ok, now that the task is ready let’s print out hello and also add the project name. Notice this  file belongs to the app module, so  will return this module’s name which is ‘app’. Instead I’ll use , which returns the name of the project.

tasks.register("hello"){
    println("Hello " + project.parent?.name)
}

Now it is time to run the task. Looking at the task list, I can see that my new task is listed here.

The new task is listed in Gradle pane in Android Studio

I can double click the hello task or execute this task through terminal and see the hello message printed in the build output.

The task prints hello message in the build output

When I check the logs, I can see that this message is printed during the configuration phase. Configuration phase is really not about executing the task function, like printing Hello World in this example. Configuration phase is a time to configure a task to influence its execution. You can tell the task about the inputs, parameters and where to put the output.

Configuration phase runs regardless of which task is requested to run. Running time consuming code in the configuration phase can result in long configuration times.

The task execution should only happen during the execution phase so we need to move this print call to the execution phase. To do that I can add  or  which will print the hello message at the beginning or at the end of the execution phase respectively.

tasks.register("hello"){
    doLast {
        println("Hello " + project.parent?.name)
    }
}

When I run the task again, this time I can see that the hello message is printed during the execution phase.

The task now prints the hello message in execution phase

Right now my custom task is located in the  file. Adding custom tasks to the  file is a simple way to create custom build scripts. However, as my plugin code gets more complicated, this doesn’t scale well. We recommend placing the custom task and plugin implementations in a  folder.

Implementing the plugin in buildSrc

Before writing any more code, let’s move my hello task to . I’ll create a new folder and name it . Next I create a  file for the plugin project so Gradle automatically adds this folder to build.

This is a top level directory in the root project folder. Notice I don’t need to add this as a module in my project. Gradle automatically compiles the code in this directory and puts it in the classpath of your build script.

Next I create a new src folder and a new class named . I convert this new class to an  class and extend . Next, I’ll add a new function called , annotate this function with  annotation and move my custom task code from  to this function.

abstract class HelloTask: DefaultTask() {    
    @TaskAction
    fun taskAction() {
        println("Hello \"${project.parent?.name}\" from task!")
    }
}

Now that my task is ready I’ll create a new plugin class which needs to implement  and override the  function. Gradle will call this function and pass in the  object. To register the , I’ll call  on  and give this new task a name.

class CustomPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.tasks.register<HelloTask>("hello")
    }
}

At this point I can also declare that my task depends on another task.

class CustomPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.tasks.register<HelloTask>("hello"){
            dependsOn("build")
        }
    }
}

Next, let’s apply the new plugin. Note that, if my project had more than one module, I could reuse this plugin by adding it to other  files as well.

plugins {
    id ("com.android.application")
    id ("org.jetbrains.kotlin.android")
}apply<CustomPlugin>()android {
   ...
}

Now I’ll execute the  task and observe that my plugin works just like before.

Now that I moved my task to , let’s take it a step further and discover the new Android Gradle Plugin APIs. AGP offers extension points on it’s lifecycle while building the artifacts.

To start with the Variant API let’s first discuss what a Variant is. Variants are different versions of your app that you can build. Let’s say besides a fully functioning app, you also want to build a demo version of your app or an internal version for debugging purposes. You can also target different API levels or device types. V​​ariants are created by combining build types, such as  and , and product flavors that are defined in the build script.

Using the declarative DSL in your build file to add a build type is perfectly fine. However, doing that in code gives your plugin a way to influence the build in ways that are not possible or difficult to express using declarative syntax.

AGP starts the build by parsing the build script, and the properties set in the  block. The new Variant API callbacks allow me to add a  callback from the  extension. In this callback I can change the DSL objects before they are used in Variant creation. I’ll create a new build type and set its properties.

val extension = project.extensions.getByName(
    "androidComponents"
) as ApplicationAndroidComponentsExtension
       
extension.finalizeDsl { ext->
    ext.buildTypes.create("staging").let { buildType ->
        buildType.initWith(ext.buildTypes.getByName("debug"))
        buildType.manifestPlaceholders["hostName"] = "example.com"
        buildType.applicationIdSuffix = ".debugStaging"
    }
}

Notice in this phase, I can create or register new build types and set their properties. At the end of this phase, AGP will lock the DSL objects, so they cannot be changed. If I run the build again, I can see that a staging version of the app is built.

Now, let’s say one of my tests is failing and I want to disable unit tests to build an internal version to figure out what is wrong.

To disable unit tests, I can use the  callback which allows me to make such changes through the  object. Here I’ll check if the current variant is the one I created for . Next, I’ll disable the unit tests and set a different  version.

extension.beforeVariants { variantBuilder ->
    if (variantBuilder.name == "staging") {
        variantBuilder.enableUnitTest = false
        variantBuilder.minSdk = 23
    }
}

After this phase, the list of components and artifacts that will be created are now finalized!

You can find full code listing for this sample below. Also if you want to see more samples like this, make sure to check out the gradle-recipes repo on Github.

Summary

Writing your own plugins, lets you extend Android Gradle Plugin and customize your build to your project’s needs!

In this article you’ve seen how to use the new Variant API to register callbacks in , use DSL objects to initialize Variants, influence which Variants are created, and their properties in .

In the next article, we will take this even further by introducing the Artifacts API, and showing you how to read and transform artifacts from your custom Tasks!

By Murat Yener
Source Medium

Previous Join Us For Google Cloud Security Talks: Zero Trust Edition
Next Giving Your Legacy Applications An API Facelift