Writing Gradle Plugins for Android; or, Donald Trump is a Huge Tool
Tags: gradle, plugin, android, kotlin
Published: 2020-01-05 23:24:47

---
title: Writing Gradle Plugins for Android; or, Donald Trump is a Huge Tool
published: true
description: learn to write a Gradle plugin that targets Android projects
tags: gradle, plugin, android, kotlin
---

So you'd like to write a Gradle plugin, but most of the tutorials out there focus on Java projects. Not only are you not targeting the JVM (we're mobile, baby!), you're not even writing in Java! So let's do this -- let's write a Gradle plugin, in Kotlin, that targets Android projects (written in Java or Kotlin).

# What you'll learn
1. How to write a Gradle plugin following modern best practices.
1. How to do it in Kotlin, and with the Kotlin DSL.
1. How to hook into Android-specific project features, such as being variant-aware by default.
1. How to verify your plugin works.
1. How to make your builds insult Donald Trump, the (somehow) president of the United States of America.

# Pre-requisites
1. You have Android Studio or IDEA installed and know how to use it.
1. You have Gradle [installed](https://gradle.org/install/) (I don't mean the wrapper). I use [SDKMAN](http://sdkman.io/) to manage my Gradle installation.
1. An understanding that Donald Trump is a huge tool.

# Donald Trump is a Huge Tool

Our playground for this tutorial will be the [Trump Insulter Plugin](https://github.com/autonomousapps/trump-insulter-plugin), which we will develop together over the course of this tutorial.

> If you're not American and would prefer to insult your own Great Leader, feel free to replace that name with, say, Scott Morrison or Boris Johnson.

# Getting started
The best way to get started on any new Gradle project is to use the `init` task. This is the only time you'll be required to use the command line. Please note the use of `gradle` and _not_ `./gradlew`! This is the only time you'll be using the Gradle distribution installed on your computer.
```
$ cd some-dir && gradle init
```
This will start an interactive flow where the task asks you what kind of Gradle project you want to create. Select "Gradle plugin" for type, "Kotlin" for implementation language, and "Kotlin" for build script DSL. Now pick a good project name and source package (defaults are based on your present working directory).

Now open your new project in your IDE and let's take a look around.

# The basics
Let's start by looking at our build script. Open the `build.gradle.kts` that was generated by the `init` task.

## Plugins
Two plugins are already applied, the `java-gradle-plugin` for developing Gradle plugins, and the `"org.jetbrains.kotlin.jvm"` plugin for editing and compiling Kotlin source. We're going to make some changes here.

```kotlin
plugins {
    `java-gradle-plugin`
    id("org.jetbrains.kotlin.jvm") version "1.3.61"
    `kotlin-dsl`
    id("com.gradle.plugin-publish") version "0.10.1"
}
```

The `kotlin-dsl` plugin will give us access to the Kotlin DSL API in our actual plugin code, and the Gradle Plugin-Publishing Plugin will be used later for publishing your plugin to the Gradle Plugin Portal. And of course we also bumped the Kotlin plugin to the latest stable version (at time of writing).

## Repositories

Add a repo to your `repositories {}` block:

```kotlin
repositories {
    jcenter()
    google()
}
```

We'll need `google()` so we can add the Android Gradle Plugin source in our `dependencies` block in just a moment.

## Group-Artifact-Version (GAV) Coordinates
In order to use your plugin in a real project, it will need GAV coordinates. There are two ways of specifying these coordinates; we'll get to the second one later. For now, add this to your build script, below `repositories {}`

```kotlin
version = "0.1.0"
group = "com.autonomousapps"
```

With this set, your plugin's default coordinates are now `"com.autonomousapps:project-name:0.1.0"`, where `project-name` is set in `settings.gradle.kts` as `rootProject.name`; this is auto-generated by the `init` task. Someone would only need to know these coordinates if they wanted to add your plugin to their `dependencies` block, in order to extend it (similarly to what we'll be doing with the Android Gradle Plugin).

## Target Java 8
In order to use Java 8 features (or 11, or 13...), you must tell Gradle what your target is. Add the following:
```kotlin
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}
```
Now Java and Kotlin source in your project will be compiled to target Java 8.

## Dependencies
Because we want to target Android projects, we need access to AGP (Android Gradle Plugin) source. Edit your `dependencies {}` block as follows:
```kotlin
dependencies {
    ...
    
    compileOnly("com.android.tools.build:gradle:3.5.3") {
        because("Auto-wiring into Android projects")
    }
}
```

This will let us compile against AGP 3.5.3 without also constraining us to only that version at runtime. The exact runtime will be provided by the project to which you apply your plugin. This implies you must carefully test your plugin against different versions of AGP. For a quick list of recent AGP versions, I like to use the [@AGPVersions](https://twitter.com/agpversions) Twitter account.

## Plugin metadata
The final thing we'll look at in the build script, for now, is the `gradlePlugin {}` block. Yours will look something like this:
```kotlin
gradlePlugin {
    // Define the plugin
    val greeting by plugins.creating {
        id = "com.example.greeting"
        implementationClass = "com.example.ExamplePlugin"
    }
}
```
The `id` is how you apply your plugin. Given the above, you would apply your plugin like so:
```kotlin
plugins {
    id("com.example.greeting") version "0.1.0"
}
```
The `implementationClass` field is the fully-qualified class name of the Plugin class. Let's change this to the following:

```kotlin
gradlePlugin {
    // Define the plugin
    val insulter by plugins.creating {
        id = "com.autonomousapps.trump-insulter"
        implementationClass = "com.autonomousapps.TrumpInsulterPlugin"
    }
}
```

And that's it for `build.gradle.kts`! (for now)

# Plugin implementation
Navigate to the auto-generated plugin class. It should be something like `com.example.ExamplePlugin`. The first thing we want to do is change its name to match what we set in the `gradlePlugin` block from earlier. So, change its name to `TrumpInsulterPlugin` and ensure the package matches as well.

> Warning: IDE refactoring tools do not ensure that the FQCN of your Plugin class matches what you set in `gradlePlugin`. You must keep these in sync manually.

With that out of the way, look at the code itself. It should look like this:
```kotlin
/**
 * A simple 'hello world' plugin.
 */
class TrumpInsulterPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        // Register a task
        project.tasks.register("greeting") { task ->
            task.doLast {
                println("Hello from plugin 'temp.greeting'")
            }
        }
    }
}
```
Here we already see the most basic and important thing about a plugin, which is that it must extend the `Plugin<T>` interface and implement the `apply(T target)` function.

> `T` can be a variety of types, including `Project` (for project plugins), `Settings` (for settings plugins), and `Gradle` (init script plugins).

You might already have an error highlighted by your IDE in this code. This is because we have added the `kotlin-dsl` plugin, which adds some syntactic sugar to the standard Gradle API you'd usually have access to while writing plugins. To fix the error, remove `task ->` and `task.` in front of `doLast`. In other words:
```kotlin
project.tasks.register("greeting") {
    doLast {
        println("Hello from plugin 'temp.greeting'")
    }
}
```
But there's still a problem. This plugin adds a task to emit a friendly greeting on execution. Let's fix that.

```kotlin
project.tasks.register("insult") {
    doLast {
        println("Donald Trump is such a huge tool")
    }
}
```
Much better.

## See it in action
Before we do anything else, it would be nice to see this in action and really _know_ that it works. Let's make that happen.

The following takes advantage of Gradle's support for [composite builds](https://docs.gradle.org/current/userguide/composite_builds.html). This is a way to _compose_ builds together that exist in separate roots or repositories.

First, select another of your projects and open it in your favorite editor. Open the `settings.gradle[.kts]` file and add the following at bottom:
```kotlin
includeBuild("../trump-insulter-plugin")
```
That string is the relative path from your other gradle project, to your plugin project. If you have this open in an IDE, you can run gradle sync and, when that's done, you should see your plugin code in the Project window at left.

Next, open `app/build.gradle[.kts]` (or similar) and add
```kotlin
plugins {
    id("com.autonomousapps.trump-insulter") version "0.1.0"
}
```
Sync and execute the `insult` task from the Gradle pane on right, or on the command line, execute `./gradlew app:insult` and bathe in the warm glow of insulting the most powerful moron on planet Earth.

## Needs more Kotlin
Now that we know our plugin works, let's iterate. The savvy reader will note there's not nearly enough Kotlin in that auto-generated plugin code. Let's fix that.

```kotlin
class TrumpInsulterPlugin: Plugin<Project> {
    override fun apply(project: Project): Unit = project.run {
        tasks.register("insult") {
            doLast {
                println("Donald Trump is such a huge tool")
            }
        }
    }
}
```
Mm, an inline extension function from the Kotlin stdlib. Much better. However, while it's true that our Kotlin quotient has increased, it must also be observed that the `project` instance is now the implicit receiver inside our `apply` block, making that code slightly more succinct.

## Needs More Android
This plugin is already pretty great, and fulfills the goal of letting you insult Trump at will, with a Gradle task. But can we do better? I think so.

```kotlin
class TrumpInsulterPlugin: Plugin<Project> {
    override fun apply(project: Project): Unit = project.run {
        pluginManager.withPlugin("com.android.application") {
            the<AppExtension>().applicationVariants.all {
                tasks.register("insult${name.capitalize()}") {
                    doLast {
                        println("Donald Trump is such a huge tool")
                    }
                }
            }
        }
    }
}
```

Whoa, that's a lot of new stuff. Let's go over it bit by bit.

```kotlin
pluginManager.withPlugin("com.android.application") { ... }
```
You know how, sometimes, you'll encounter a Gradle plugin, and the README says "apply this plugin _last_"? `pluginManager.withPlugin()` solves that problem. It tells Gradle to apply the code inside the block immediately after the specified plugin has been applied. Checkout the [javadoc](https://docs.gradle.org/current/javadoc/org/gradle/api/plugins/PluginManager.html#withPlugin-java.lang.String-org.gradle.api.Action-) for more information.

```kotlin
the<AppExtension>()
```
The what now? `the` is an _inline reified extension function_ on `Project` (so much Kotlin!) that makes it trivially easy to configure various bits of DSL contributed by other Gradle plugins. If you combine that with the knowledge that `AppExtension` is the type that you're configuring when you are editing an `android {}` block in a build script, then the way to read this line is "the app extension". It's meant to be read like prose, hence "the" weird name.

> This block is inside `pluginManager.withPlugin(...)` because otherwise we would have no assurance that the Android plugin has been applied, and with it an object of type `AppExtension` made available.

```kotlin
the<AppExtension>().applicationVariants
```

You can think of `applicationVariants` as a collection of `ApplicationVariant` instances -- one for each _variant_ in your app project. Standard variants are "debug" and "release", but if you use product flavors, you will have variants like "flavor1Debug", "flavor2Debug", etc.

```kotlin
the<AppExtension>().applicationVariants.all { ... }
```

`all {}` is a Gradle method for iterating over a particular kind of container. The exact type is outside the scope of this tutorial ([DomainObjectCollection](https://docs.gradle.org/current/javadoc/org/gradle/api/DomainObjectCollection.html)). For our purposes, it is sufficient to think of it as a live collection. `all {}` will execute the supplied action against all current _and future_ members of the given collection. This is the method that saves us from having to use `Project.afterEvaluate` and waiting for the `android {}` block to finish being configured.

```kotlin
the<AppExtension>().applicationVariants.all {
    tasks.register("insult${name.capitalize()}") {
        doLast {
            println("Donald Trump is such a huge tool")
        }
    }
}
```
And finally, the task. See how we've changed the name to be dynamic. Now we'll have tasks like:
1. `insultDebug`
2. `insultRelease`

Not only is this Cool :tm:, but it's also necessary. Without this, Gradle would complain about us trying to add multiple tasks with the same name, which is not allowed.

**Give it a try**. Sync your project and execute your new tasks.

## I still don't think there's enough Android
I think you're right. The plugin is better than ever, capable of insulting Trump _per-variant_, but is it enough? I mean, we still have to manually trigger the task, which seems kind of lame. Can we make it more... automagic?

Yes. Yes, we can.

### IDE magic
Maybe there's something in `applicationVariants` that could help? Let's find out. Use your IDE to navigate to source on that method call. You should see
```java
public DomainObjectSet<ApplicationVariant> getApplicationVariants() {
    return applicationVariantList;
}
```
Ignore most of that for purposes of this tutorial. We must go deeper. Navigate to source on `ApplicationVariant`.

![We need to go deeper](https://i.imgur.com/i3SFJPP.jpg)

```java
/**
 * A Build variant and all its public data.
 */
public interface ApplicationVariant extends ApkVariant, TestedVariant {}
```
Well, that was underwhelming. Is that really it? Let's navigate to source one more time. Take a deeper look at `ApkVariant`.

![We need to go deeper](https://i.imgur.com/i3SFJPP.jpg)

```java
@Nullable
TaskProvider<PackageAndroidArtifact> getPackageApplicationProvider();
```

Jackpot! That's what we want. We can use the `TaskProvider` returned by that method to hook our `insult` tasks up to the APK-packaging task, and insult Trump on each and every build! Let's see what that looks like:

```kotlin
class TrumpInsulterPlugin: Plugin<Project> {
    override fun apply(project: Project): Unit = project.run {
        pluginManager.withPlugin("com.android.application") {
            the<AppExtension>().applicationVariants.all {
                val insultTask = tasks.register("insult${name.capitalize()}") {
                    doLast {
                        println("Donald Trump is such a huge tool")
                    }
                }
                
                packageApplicationProvider.configure { 
                    finalizedBy(insultTask)
                }
            }
        }
    }
}
```

First we get a reference to the `TaskProvider` returned by `tasks.register(...)` and then we tell the `packageApplicationProvider` `TaskProvider` that our task finalizes it. This just means it will execute our task _after_ it finishes its own work.

**Let's see it in action.**
```
$ ./gradlew app:assembleDebug
```
(or use your IDE)

```
> Task :app:insultDebug
Donald Trump is such a huge tool

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/6.0.1/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 40s
77 actionable tasks: 63 executed, 10 from cache, 4 up-to-date
```

:heart:

# Oh but wait
We've come this far. What if we want to publish this plugin to a public repository, so _anyone_ can easily insult Donald Trump during each of their Android builds? Easy as pie. You'll remember we already added the `"com.gradle.plugin-publish"` plugin to our project. This lets us publish directly to the Gradle Plugin Portal (available via `gradlePluginPortal()` in a `repositories {}` block, and added by default to all Gradle builds).

Full instruction for using the Gradle Plugin-Publishing Plugin are outside the scope of this tutorial, and anyway the Gradle [docs](https://plugins.gradle.org/docs/publish-plugin) cover it well. For our purposes, the  main thing you need to do is add this to your build script:

```kotlin
pluginBundle {
    website = "https://path/to/your/website"
    vcsUrl = "https://path/to/your/git/repo"

    description = "A plugin to insult Donald Trump, apparently the president of the United States"

    (plugins) {
        "trumpInsultingPlugin" {
            displayName = "Trump Insulter"
            tags = listOf("insults", "tutorial")
        }
    }
}
```

> Please read the docs carefully. You will need to generate a key and a secret for publishing to the Plugin Portal.

With your Plugin Portal account set up and your build script configured to publish your plugin, all that's left is to do it! From your IDE, open the Gradle tasks pane and expand the "plugin portal" group. You'll see `login` and `publishPlugins`. `login` is for when you don't have your key and secret available as project properties (read the docs!), and `publishPlugins` does what it says. Let's do it:
```
$ ./gradlew publishPlugins
```

And the result (for me):
```
> Task :publishPlugins
Publishing plugin com.autonomousapps.trump-insulter version 0.1.0
Publishing artifact build/libs/gradle-plugin-trump-insulter-0.1.0.jar
Publishing artifact build/libs/gradle-plugin-trump-insulter-0.1.0-sources.jar
Publishing artifact build/libs/gradle-plugin-trump-insulter-0.1.0-javadoc.jar
Publishing artifact build/publish-generated-resources/pom.xml
Activating plugin com.autonomousapps.trump-insulter version 0.1.0
```

:heart: :heart:

# The code
You can find all the code on Github at https://github.com/autonomousapps/trump-insulter-plugin, and instructions for adding to your Gradle projects are available at the Gradle Plugin Portal at https://plugins.gradle.org/plugin/com.autonomousapps.trump-insulter.

# Extra credit
There's so much more we could do! We could extract our `insult` task into its own class and make it configurable (what if we want different insults for different days of the week?). We could add an `insult {}` extension to enable easy configuration for users. Maybe we want a boolean like `isClean` (defaults to `true` for the kids, but can be set to `false` for adult eyes). Maybe we want to post a message to Slack, or send a Tweet, or _anything_? Sky's the limit, my friends.