Gradle and AGP build APIs: How to write a plugin

Murat Yener
Android Developers
Published in
6 min readNov 24, 2021

--

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 build.gradle.kts file, I’ll register a new task and name this task ‘hello’.

tasks.register(“hello”){ }

Ok, now that the task is ready let’s print out hello and also add the project name. Notice this build.gradle.kts file belongs to the app module, so project.name will return this module’s name which is ‘app’. Instead I’ll use project.parent?.name, 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 doFirst() or doLast() 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 build.gradle.kts file. Adding custom tasks to the build.gradle 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 buildSrc folder.

Implementing the plugin in buildSrc

Before writing any more code, let’s move my hello task to buildSrc. I’ll create a new folder and name it buildSrc. Next I create a build.gradle.kts 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 HelloTask. I convert this new class to an abstract class and extend DefaultTask. Next, I’ll add a new function called taskAction, annotate this function with @TaskAction annotation and move my custom task code from build.gradle.kts 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 Plugin and override the apply() function. Gradle will call this function and pass in the Project object. To register the HelloTask, I’ll call register() on project.tasks 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 build.gradle files as well.

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

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

./gradlew hello

Now that I moved my task to buildSrc, 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 debug and release, 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 android block. The new Variant API callbacks allow me to add a finalizeDSL() callback from the androidComponents 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 beforeVariants() callback which allows me to make such changes through the VariantBuilder object. Here I’ll check if the current variant is the one I created for staging. Next, I’ll disable the unit tests and set a different minSdk 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 AndroidComponentsExtension, use DSL objects to initialize Variants, influence which Variants are created, and their properties in beforeVariants().

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!

--

--