Gradle Convention Plugins

For me, Gradle was one thing that was there only to throw errors from time to time: incorrect library import, broken file cache, AGP incompatibilities, etc.

But then I realized how powerful is Gradle for automation to save hours of tedious work. One of the drawbacks it had (in my opinion) was Groovy, the script language used to write scripts and plugins for Gradle, but now you can use Kotlin DSL, which is better structured and it is like coding “normal” Kotlin code.

What things can you do with a Gradle Plugin? Many things, for example:

  • A system to download a Google Spreadsheet with the app localization, parse it, and generate Kotlin code using Kotlin Poet.
  • You can generate scripts with common configuration if you have a multi-module app so that you avoid repeating code in build.gradle.kts files.
  • You can fetch the Figma API and generate code for a design system.

That sounds good eh?. Let’s begin!

Create build-logic module

The first step is to create a module named ‘build-logic’ inside our project. To do that follow these steps:

  • Create a folder named build-logic inside the project
  • Create a folder src/main/kotlin inside it
  • In the build-logic folder, create 3 files:
    • build.gradle.kts
    • settings.gradle.kts

Ok, now we need to fill these files with something.

First, settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("versionCatalogLibs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}
rootProject.name = "build-logic"

Take a look at the code, we are declaring which repositories we are going to use, and we are creating a variable named “versionCatalogLibs” to be able to reference libraries inside our plugins. Lastly, we are naming the root project with the same name as the module.

Second, build.gradle.kts:

plugins {
    `kotlin-dsl`    
}

group = "codepredator.android.buildlogic"

repositories {
    mavenCentral()
    google()
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
    
}

gradlePlugin {
    plugins {
        register("myPlugin") {
            id = "codepredator.plugin.myplugin"
            implementationClass = "MyPlugin"
        }        
    }
}

This is a pretty standard build.gradle.kts file. Configure it accordingly depending on your project requirements. For this example I use Java 17

Now you can create a class named MyPlugin inside the src/main/kotlin folder with this content in it:

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {
    
    override fun apply(target: Project) {
        println("Plugin added!")
    }
    
}

With this, we are ready to start a Gradle sync to se our plugin in action (well, it does nothing, but it will do something 🙂

Before syncing, we need to add a line to the root project settings.gradle.kts to “transform” the build-logic module in something usable by Gradle:

pluginManagement {
    includeBuild("build-logic") //Add this line!!
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}

After that, run a Gradle sync and you should read “Plugin added!” somewhere in the log but… nothing appears. What’s happening?. Easy, we are not using the plugin, we have just declared it. Go to the build.gradle.kts file of any of you app or libraries modules and add this line inside the plugins section and the very beginning of the file:

    id("codepredator.plugin.myplugin")

And now, start a Gradle sync, and you will see this in the log:

> Task :build-logic:checkKotlinGradlePluginConfigurationErrors
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:compileKotlin
> Task :build-logic:compileJava NO-SOURCE
> Task :build-logic:classes
> Task :build-logic:jar

> Configure project :mylibrary
Plugin added!

> Task :prepareKotlinBuildScriptModel UP-TO-DATE

BUILD SUCCESSFUL in 6s
5 actionable tasks: 5 executed

Ok, we have a plugin…which does nothing :D. Let’s see a real example of the usefulness of Gradle convention plugins

My quotes plugin

I’m developing a quotes app and I have the quotes info in a Google SpreadSheet document so I’ve made a plugin to download the sheet, parse it and generate a class with all the required info automatically. Every time I update the sheet I just need to execute a Gradle task to regenerate the quotes file, no manual work required, no manual errors added. Let’s see:

class QuotesAndCategoriesPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        println("Applying quotes and categories plugin...")

        with(target) {
            tasks.create(
                "generateQuotesAndCategories",
                GenerateQuotesAndCategoriesTask::class.java
            ) {
                group = "quotes"
            }.dependsOn("downloadQuotes")

            tasks.create("downloadQuotes", DownloadQuotesTask::class.java) {
                group = "quotes"
            }
        }
    }

}

This is a really simple plugin which add two Gradle tasks to a group named “quotes”. You can see the set-up is really simple as this is only a task name -> task class link.

Let’s see the content of ‘downloadQuotes’ task:

open class DownloadQuotesTask @Inject constructor(private var projectLayout: ProjectLayout) :
    DefaultTask() {

    @TaskAction
    fun perform() {
        println("Downloading quotes...")

        //Quotes
        val quotesPath = projectLayout.projectDirectory.file(QuotesConstants.QUOTES_PATH).toString()
        var sourceUrl = TasksUtils.generateSpreadSheetUrl("kjsdhgfslkj")
        TasksUtils.downloadToFile(ant, sourceUrl, quotesPath)

        //Categories
        val categoriesPath =
            projectLayout.projectDirectory.file(QuotesConstants.CATEGORIES_PATH).toString()
        sourceUrl = TasksUtils.generateSpreadSheetUrl("lahsfgsalkjja")
        TasksUtils.downloadToFile(ant, sourceUrl, categoriesPath)
    }

}

I’m not going to enter in details about how to create tasks, maybe in a future tutorial. Take a look to the @TaskAction annotation and how the projectLayout can be injected by Gradle in the task to operate with files. This task will appear in the Gradle tasks list in the right side of Android Studio so that you can execute it easily (you can also execute it using the command line)

And that’s it!. This is only a briefly introduction to Gradle convention plugins. There’s a lot more things you can do with this powerful tool. Use it whenever you have tedious and/or repetitive work to do!

See you in the next tutorial!

, , ,