In this post I'm going to detail the steps I went through to create a Gradle plugin for Android from scratch.

I've been meaning to write this post for a while, ever since I did my first post Monkeying around with gradle to generate code and the recent Caster.IO post by Annyce Davis - Gradle Plugin Basics - inspired me to actually get it done.

I've found it very difficult to create a standalone gradle plugin for Android Studio. There's a lot of documentation around, but it always seems to be missing something. Have you ever tried doing a Google search for create a gradle plugin for android? I have and the results that I get are pretty lack-lusture.

Even Gradle's own documentation on the subject only helps a little. The section on creating a standalone project pretty much consists of telling you to add two lines to your build.gradle and adding a properties file. It doesn't tell you how to build it. The publishing section assumes you know about publishing and want to publish it to the public. What about if I just want to build it to run it myself to see if the bloody thing works?

And what about if you want it to run as part of your Android Studio build process? A little help?

I'm not going to cover every possibility of everything you might want to do in this one little blog post, but hopefully it'll be a good starting point based on what I've learnt.

Anyway, enough chatter. Let's get to it.

Step 0 - Assumptions

I'm going to assume that you are already set up for Android development, that is you have Java installed, Android Studio and the appropriate build tools. I think it's a safe assumption; writing a Gradle plugin isn't the first thing you want to be doing as a brand-spanking new Android developer.

From your point you have to assume that I'm not an expert on Gradle. I really don't know that much about it. What I've written here is what I've found out and what works for me. You can also assume that I'm devilishly handsome and a great conversationalist if you wish. However those aren't necessary for the purposes of this blog post.

Step 1 - Getting set up

This is the one thing that I could find no decent information on at all; how to even get set up.

Install groovy and gradle. On my Mac I used Homebrew. You might be better off downloading directly.

Android Studio is a customised IntelliJ. As such it isn't suitable for writing standalone Gradle plugins. If you try to create a new project it will be an Android project. This is fair enough. That's what it's for. So the first tip is to download and install IntelliJ.

Go, grab the community edition from https://www.jetbrains.com/idea/

This post was written using version 15.0.3. If you're reading this in the future and I haven't updated it for the new version then you'll have to wing it and try to work out what I'm on about.

It's free, has what you'll need, and will be hauntingly familiar if you've been using Android Studio. (Sorry for those still using Eclipse.)

Install it and when you run it for the first time just accept all the install defaults. If you really want to mess around with the plugins at least make sure you have groovy, gradle and maven integration installed.

When you're sitting, staring at the IntelliJ IDEA window, hit Create New Project and the fun begins. Hopefully you'll see a screen that looks a little like this:

New Project

If Gradle is missing from the left or Groovy is missing from the right then it means one of the needed plugins is missing. In this case hit Cancel and from the main screen hit Configure -> Plugins to enable more plugins and try again.

If your Project SDK isn't showing a Java version, hit the New... button and locate your Java install directory.

Tick the Groovy option and then hit Next

On the next screen you need to fill in the GroupID and ArtifactId. Whilst these can be anything, let's stick to convention. Make the GroupID the same as the Company Domain you use for your Android apps and the ArtifactId is the name for this plugin you are going to create. (For this blog post I'm using com.afterecho and blogpost for GroupID and ArtifactId respectively.) Leave the Version alone for now at 1.0-SNAPSHOT and hit Next

Don't change anything on the next screen. It recommends "Use default gradle wrapper" and who are we to argue? Hit Next.

You probably don't want to change anything on the next screen unless you want to move your project to a different directory. When you're done, hit Finish.

Step 2 - The journey begins

Once IntelliJ has created your project you'll notice a complete lack of any source code directories. At the top of the Project tree, right click, go to New and then Directory.

New Project

Enter the name src/main/groovy. If all is well the groovy directory will have a different colour to indicate that it is a source directory.

New Project

If you right-click on the groovy directory you'll see that, under New, you can now create packages and classes rather than just files and directories. Create an appropriate package and then a Groovy class in that package for your plugin.

Modify the class definition to add implements Plugin<Project> so it looks like

1
2
class BlogPlugin implements Plugin<Project> {
}

and what you'll find is the Project class is unknown by IntelliJ. If you try to use IntelliJ to auto-import it, it won't happen.

Step 3 - Building blocks

We're missing something. Why can't we import Project? The documentation from Gradle says that we need to add

dependencies {
  compile gradleApi()
  compile localGroovy()
}

into our build.gradle. Great. Add those two lines to the dependencies section that already exists in the build.gradle.

By the way, when you load the build.gradle for the first time IntelliJ will probably say "You can configure Gradle wrapper to use distribution with sources. It will provide IDE with Gradle API/DSL documentation." It's probably a good idea, so hit "Ok, apply suggestion." If you don't see this then it probably means your copy of IntellJ doesn't like you but don't worry about it; it won't affect what we're doing here.

Once the two dependencies have been added to your build.gradle hop back to our plugin class and user the power of IntelliJ to import Project, right? Maybe.

If you added the dependencies in before hitting the "Ok, apply suggestion" then you can. There are multiple options so make sure you choose the org.gradle.api version.

If you did it in the other order then you will find that the org.gradle.api version of the Project class is missing. On my install I only get org.apache.tools.ant.Project which is wrong.

I'm used to Android Studio and when you add a dependency in the build.gradle it tells you that you need to sync the project. IntellJ doesn't do this. You have to do it yourself. And you'll have to do it every time you add a new dependency to your build.gradle.

To do it, go to the View menu, then to Tool Windows and then to Gradle.

Gradle tool window

When the Gradle tool window pops out, hit the refresh button in the top left.

Refresh

You would not believe how much time I wasted trying to work out why I couldn't import Project.

Once you've imported Project and Plugin you'll see the red squiggly line of doom under your class definition. Time to implement a method.

Step 4 - Apply yourself

The missing method is apply(). Use IntelliJ's intentions to add this in automatically (CTRL + Return, ALT + Return depending on your O/S and keymap).

This is where the magic happens. If you were to put a println in apply() you'd see in your Android build that this happens right at the start of the build process, before anything else really happens. Not useful.

So what's it for? Here we can create a Gradle Task to do the work at an appropriate time.

(To make the rest of this easier to follow assume that the plugin class I'm creating is called BlogPlugin in the package com.afterecho.gradle. Note that the package name for your plugin doesn't have to be the same at the GroupID configured when creating the project.)

This is where we create a Gradle Task. In the simplest case we can create one task that is executed only when we explicitly request it rather than as part of the normal build flow. An example of this in the standard set of tasks for Android is the uninstallAll task that uninstalls all builds and flavours of builds of your app from all attached devices. uninstallAll doesn't run as part of your normal build process. You have to manually run it.

Let's create a task called showDevices that simply executes adb devices.

Make the apply() method look like this:

1
2
3
4
5
6
7
8
9
@Override
void apply(Project target) {
    def showDevicesTask = target.tasks.create("showDevices") << {
        def adbExe = target.android.getAdbExe().toString()
        println "${adbExe} devices".execute().text
    }
    showDevicesTask.group = "blogplugin"
    showDevicesTask.description = "Runs adb devices command"
}

Simple. A new task is created with the name showDevices. This task has an action that gets the path to adb and runs a command. The project (target) has an android property that we can use to get some useful information. In this case, the path to the adb command.

The setting of the description is just to be nice to people who run ./gradlew tasks or like to look at tooltips in Android Studio so they can see what our task does. The setting of the group is to make it easier to find in Android Studio and the gradle tasks output.

Ultimately our class should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.afterecho.gradle

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

class BlogPlugin implements Plugin<Project> {
    @Override
    void apply(Project target) {
        def showDevicesTask = target.tasks.create("showDevices") << {
            def adbExe = target.android.getAdbExe().toString()
            println "${adbExe} devices".execute().text
        }
        showDevicesTask.group = "blogplugin"
        showDevicesTask.description = "Runs adb devices command"
    }
}

How do we use the plugin now that we've written it? Another source of frustration. I tried many ways to create JAR files that could be dropped in places and I got nowhere. I didn't want to publish my nascent plugin to Maven Central or JCenter.

Step 5 - When life gives you repos...

You can create a maven-like repo on your local file system. Yay!

Create a directory somewhere on your machine. For this we need to modify the build.gradle again to add the following to the end

apply plugin: 'maven'

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: uri('repo'))
        }
    }
}

Replace 'repo' with the full path to your directory. If you just put 'repo' like I have then it will go into a 'repo' directory within your plugin's directory, which has the advantage that you can see it in Intellij.

Don't forget to bring out the Gradle tool window and hit refresh! Leave the tool window open and you should now see a new task; uploadArchives

Upload Archives

Double-click on uploadArchives to run it. The first time you'll get a message in the console window saying something about not being able to find metadata. Don't worry. The metadata gets created and subsequent runs will not complain.

If you look in your repo directory you will find something like this

The Repo

It's your very own maven repo. :)

You can see that all the files have a timestamp in them. This is because you have specified this build is a snapshot (remember the version number right at at the beginning?) Every time you run the uploadArchives task it will create another set of files.

To stop ourselves from getting overloaded, change the version in your build.gradle to remove the "-SNAPSHOT", delete all the files in your repo directory and run uploadArchives again.

Our plugin still isn't quite ready to use. There's one missing file.

Step 6 - Searching for plugins (in all the wrong places)

We need one more file in our archive that tells Gradle which class our plugin is in. Back in our project structure, right click on "main" and create a directory resources/META-INF/gradle-plugins

Another New Directory

In this directory create a new File. The filename is the name you want to give your plugin (the PluginId) followed by ".properties". In the build.gradle for a standard Android app there's a line

apply plugin: 'com.android.application'

so the file for this would be com.android.application.properties

I don't recommend you call your plugin com.android.application so let's call the file com.afterecho.blogplugin.properties

In this file put a single line; implementation-class= and the fully qualified name of the class that extends Plugin<Project>. In my case it is

implementation-class=com.afterecho.gradle.BlogPlugin

Step 7 - Publish and be damned

Run the uploadArchives task again. Now we can leave IntellJ alone for a while and go to the totally different environment of Android Studio.

Create a new Android application or load one that you prepared earlier. Open the Project level build.gradle and in the repositories section of the buildscript section, add in

maven {
    url uri('/path/to/the/repo/directory/created/earlier')
}

(Change the path as appropriate for where you put your repo earlier.) Your project now has a new place to look for plugins.

In the dependencies section of buildscript add in

classpath 'com.afterecho.gradle:blogpost:1.0'

This declares the dependency of our Android build on our plugin. The name here relates back to when the plugin was created. The bit before the first colon is the GroupId, the bit between the colons is the ArtifactId

If you created a new project, it should look something like this:

buildscript {
    repositories {
        jcenter()
        maven {
            url uri('/tmp/repo')
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.afterecho:blogpost:1.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

Now open the Module level build.gradle and, after the apply plugin: com.android.application add

apply plugin: 'com.afterecho.blogplugin'

This is where the PluginId comes in - remember the name of the properties file?

To clarify there is

  • GroupId
  • ArtifictId
  • PluginId

The PluginId can be - but probably shouldn't be - totally unrelated to the GroupId and ArtifictId.

Android Studio should be asking you to Sync Now. Go for it. A bit of an anti-climax isn't it? Nothing seems to have happened.

Step 8 - The Grand Finale

The reason is we created the task but we haven't called it. You can run the task within Android Studio from the Gradle tool window.

Task Running

Look in the "blogplugin" section for showDevices. If you hover your mouse over it you'll see the description we set earlier pop up in a tooltip. Double-click on showDevices and behold the output in the run tool window.

You can also run it from the command line within the applications project directory. By running

./gradlew tasks

you should see in the "Blogplugin tasks" section our "showDevices" task, along with the description. Run it with

./gradlew showDevices

There we have our first Gradle plugin for Android. I know it's not much, but it's a start. In a future blog post I hope to expand the plugin to something that's a little more useful.

Darren @ Æ


Comments

comments powered by Disqus