Welcome to the second article in the MAD Skills series on Gradle and AGP building apis. You have learned the basics of Gradle and how to configure the Android Gradle Plugin from the previous article “Gradle and AGP Build API: Configuring your Build file”. In this article, you’ll learn how to extend your build by writing your own plug-ins. If you prefer to see this in video, check it out here.

Starting with version 7.0, the Android Gradle Plugin provides a stable extension point for manipulating variant configurations and generated build artifacts. Some parts of the API were recently completed, so I will be using version 7.1 AGP for this article (which was in Beta when I wrote this article).

Gradle Task

I would start with a brand new project. If you want to learn synchronously, you can create a new project by selecting the base Activity template.

Let’s start by creating a Task and printing it out — yes, hello World. To do this, I will register a new Task in the build.gradle. KTS file in the application layer and name it “Hello”.

tasks.register("hello"){ }
Copy the code

Now that the Task is ready, we can print “hello” and add the project name. Note that the current build.gradle. KTS file belongs to the application module, so project.name will be the name of the current module “app”. If I use project. Parent? .name will return the name of the project.

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

It’s time to run the Task. If you look at the Task list, you can see that my Task is already in it.

The new Task is listed in the Gradle pane of Android Studio

I can double-click Hello Task or execute the Task from the terminal and watch the Hello message it prints in the build output.

△ Hello message printed by Task in the build output

When I look at the log, I can see that this information was printed during the configuration phase. The configuration phase is actually independent of the function that performs the Task, such as printing Hello World in this case. The configuration phase is the phase where tasks are configured for their execution. At this stage, you can determine the input, parameter, and output positions of the Task.

The configuration phase is executed regardless of which Task the request runs. Time-consuming operations during the configuration phase may lead to a long configuration time.

Task execution should only happen in the execution phase, so we need to move the print call to the execution phase. I can do this by adding the doFirst() or doLast() functions, which print hello messages at the beginning and end of the execution phase, respectively.

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

When I run the Task again, I can see that the Hello message is printed at execution time.

Task now prints hello messages during execution

My custom Task is currently in the build.gradle.kts file. Adding custom tasks to build.gradle files is a convenient way to create custom build scripts. However, as my plug-in code became more complex, this approach was not conducive to scaling. We recommend placing custom tasks and plug-in implementations in the buildSrc folder.

Implement the plug-in in buildSrc

Before writing any more code, let’s move Hello Task to buildSrc. I’ll create a new folder and name it buildSrc. Next, I create a build.gradle. KTS file for the plug-in project, so gradle automatically adds this folder to the build.

This is the top-level directory in the project root folder. Note that I don’t need to add it as a module in my project. Gradle automatically compiles the code in the directory and adds it to the classpath where you build your script.

Next, I create a new SRC folder with a class called HelloTask. I changed the new class to the Abstract class and made it inherit DefaultTask. Next, I’ll add a function called taskAction, annotate it with @taskAction, and migrate my custom Task code to this function.

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

Now my Task is ready. I’ll create a new plug-in class that implements the Plugin type and overrides the apply() function. Gradle calls this function and passes in the Project object. To register HelloTask, I need to call Register () on project.tasks and name the new Task.

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

At this point, I can also declare my Task to depend on other tasks.

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

Let’s apply the new plug-in. Note that if my project contains multiple modules, I can reuse this plug-in by adding it to other build.gradle files.

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

Now, I’ll run Hello Task and watch the plug-in run as before.

./gradlew hello

Now that I’ve moved my Task to buildSrc, let’s take it a step further and explore the new Android Gradle Plugin API. AGP provides extension points for its lifecycle when building artifacts.

Before we begin studying the Variant API, let’s first understand what a Variant is. A variant is a different version of your application that you can build. Suppose that in addition to a fully functional application, you want to build a demo application or a build for debugging. You can also target different APIS or device types. Variations are a combination of multiple build types, such as DEBUG and Release, as well as product variants defined in build scripts.

Adding build types in your build files using declarative DSLS is perfectly fine. However, it is impossible, or difficult to express in declarative syntax, for your plug-in to affect the build in this way in your code.

AGP starts the build by parsing the build script and properties set in the Android block. The new Variant API callback allows me to add the finalizeDSL() callback from the androidComponents extension. In this callback, I can modify DSL objects before they are applied to Variant creation. I will 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"}}Copy the code

Note that in this phase, I can create or register new build types and set their properties. At the end of the phase, AGP locks DSL objects so that they cannot be changed. If I run the build again, I can see that the staging version of the application has been built.

Now, suppose one of my tests fails, at which point I want to disable unit testing to build a build to figure out what the problem is.

To disable unit tests I can use the beforeVariants() callback. This callback allows me to make such changes through the VariantBuilder object. At this point, I check if the current variant is the one I created for Staging. Next, I’ll disable unit testing and set up a different version of minSdk.

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

After this stage, the list of components and artifacts to be created are determined.

The complete code for this example is as follows. For more examples of this, check out the Github Gradle-recipes warehouse:

import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class CustomPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.tasks.register("hello"){ task->
            task.doLast {
                println("Hello "+ project.parent? .name) } } val extension = project.extensions.getByName("androidComponents") as ApplicationAndroidComponentsExtension
        extension.beforeVariants { variantBuilder ->
            if (variantBuilder.name == "staging") {
                variantBuilder.enableUnitTest = false
                variantBuilder.minSdk = 23
            }
        }
        extension.finalizeDsl { ext->
            ext.buildTypes.create("staging").let { buildType ->
                buildType.initWith(ext.buildTypes.getByName("debug"))
                buildType.manifestPlaceholders["hostName"] = "internal.example.com"
                buildType.applicationIdSuffix = ".debugStaging"
                // Add this line of code later when explaining beforepatterns.
                buildType.isDebuggable = true 
            }
        }
    }
}
Copy the code

conclusion

By writing your own plug-ins, you can extend the Android Gradle Plugin and customize your build to your project’s needs!

In this article, you’ve learned how to use the new Variant API to register callback, using a DSL in AndroidComponentsExtension object initialized Variant, influence the Variant has been created, And their attributes in beforeVariants().

In the next article, we’ll take a closer look at Artifacts APIS and show you how to read and transform Artifacts from your custom Task.

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!