This is the second part of an undecided number of posts about creating a Gradle plugin for Android. Part one can be found here.
First up I'd like to thank everyone for their feedback on my previous post. It seems that I'm not the only one who found the documentation on creating a standalone Gradle plugin a little lacking.
Last time out we left our plugin whole, but rather lacking in substance. If you recall it just
added an extra task that ran adb devices
. Let's try for something a little more
adventurous.
(A quick aside. To make the code listings below a little shorter and more concise I haven't included the package definition or the imports. And whilst we're on the side I'm going to assume that you've done part one and you have an Android Studio project open that uses the plugin we're creating.)
Step 0 - Striking out on our own
We're going to start by moving our simple task into it's own class. It's all very well coding it straight into the Plugin class, but it's going to get a bit unwieldy after a while.
Create a new Groovy class - ShowDevicesTask - and make it extend DefaultTask. You'll see that there aren't any red squigglies indicating that there's a missing method.
To specify the method to be called when we execute our task we annotate it with @TaskAction
.
Move the implementation
of the showDevices
task out of the BlogPlugin and into the ShowDevicesTask class and take the group
and description
to make them fields of the class, so it looks something like this:
1 2 3 4 5 6 7 8 9 10 |
|
Note that there is one subtle difference here. When the implementation was part of the
BlogPlugin
the Gradle Project was passed in to the apply()
method as the target
parameter. In our Task there are no parameters. Luckily we can get the Gradle Project
via the project
field of our task. A simple swap in our adbExe
def and we're done.
Back in the BlogPlugin
class change the apply()
method to create the task from the new class:
1 2 3 |
|
Running the uploadArchives
task in IntelliJ and Sync Project with Gradle Files
in the
Android Studio project and you'll see...well, not a lot has changed. In fact nothing has
changed. Make sure the showDevices
task still exists and runs. Yay!
But at least we have our Task in it's own class. That's something...right?
Ok, I know. I promised that the task would do something a bit more interesting. How about if we take a file of words - one per line - and make our plugin create a class that contains an enum with all the words in it? That should be a little more interesting. Not much I grant you, but a little.
Step 1 - A little detour
As you probably know there are two "build types"
by default with an Android project; debug and release. A large number of the Gradle build
tasks for Android will have two versions, one for each. So there's an assembleDebug
and
assembleRelease
for example. Of course you can create more build types of your own and
then you'd have a task for each type.
You're probably also aware that you can have different flavours for your app too. By default there aren't any, but if you add some you will get even more build tasks; there will be one per build type per flavour. So if you kept with the two standard build types and created an "internal" and "external" flavour for example you would end up with the following assemble tasks
- assembleInternalDebug
- assembleExternalDebug
- assembleInternalRelease
- assembleExternalRelease
Each one of these is known as a variant. You can get a list of the variants from the Android
plugin which is available from the Gradle project passed into the apply()
method as a field called android
. To make things a little
simpler we are only going to concern ourselves with a plugin for an Android applications, not a library.
In the apply()
method of our plugin we can reference target.android.applicationVariants.all
to get a list of all the variants.
Why am I telling you this now? All will become clear...
Step 2 - The ins and outs
I'm not going to try and create a class file directly for our enum. What we will do with our plugin is to read the list of words from a file and write out a Java source file that will get compiled into our app as sure as if we had coded it ourselves. To do this we need to know where to get the words from and where to write the Java file out.
Start by creating a new Groovy class called WordsToEnumTask
, make it extend DefaultTask
as our ShowDevicesTask
did. Add a couple of String fields for description and group
and add an empty method called makeWordsIntoEnums
that is annotated with @TaskAction
.
It should look a little like
1 2 3 4 5 6 7 8 |
|
We're going to put our words in a file called plugin_words.txt
that will live at
the top level of our app's module. Unless you've changed it, the module is called
'app' and so is the directory that the file will live in. If you created a test
app in Android Studio to play along look for the directory that has the proguard-rules.pro
in it. In the Android Studio "Project" view it will look like this:
Create a file and put a handful of words in it, one word per line. As we are coding for information and learning we won't be doing any real error handling, so make the file simple; one word per line, no blank lines, no duplicates and make each word a valid Java symbol that you would see in an enum.
How do we locate this file from within our task? As it turns out, pretty easily.
There's a lovely field we can access from our project that is passed in to our
apply()
method in our plugin called projectDir
. This is
the top level of our module, which is exactly where we have just placed our
file of words.
Writing the output file is almost as simple; buildDir
gives us the root
of the build directory, which is were things are placed during the build process.
Unless you've done something really weird, this is likely to be a directory called
build
. Confusingly the Android project has one build directory at the top level
and one in each module. We're looking at the module one.
Within build there's a generated
directory, and within that a source
directory.
I can't find the post at the moment but I'm pretty sure the advice I read a while ago was to create
your own directory for your plugin to place its generated source in. So we will.
Remember back to the little detour above? Here's where it kicks in.
Each variant has a dirName
field and we use this to place our Java file. The
result of all of this is that we will need to create a task per variant that
writes our Java source file into a variant-appropriate directory.
Add this to the apply()
method in BlogPlugin
:
1 2 3 4 5 |
|
We are looping over all the variants, finding the input file and the output directory, and creating a new task for each variant. As we are creating multiple tasks in the loop we need to make name of each task unique. So we take the variant name, give it a capital letter at the start to make it feel important, and stick that on the end of "wordsToEnum".
If we uploadArchives
in IntelliJ and sync our project in Android Studio we will have a wordsToEnumDebug
task and a wordsToEnumRelease
task in our blogplugin section of Gradle tasks.
If you want to try a little experiment, open the build.gradle
on your Android app and in the
android
section add this:
productFlavors {
dev {}
qa {}
}
When you sync your project you'll find four wordsToEnum tasks; wordsToEnumDevDebug, wordsToEnumDevRelease, wordsToEnumQaDebug and wordsToEnumQaRelease. Each flavour has a buildtype version.
Remove the productFlavours section to avoid confusion for now.
Step 3 - Passing the info to the task
To get the input file and output directory information into the task we can take advantage of the
power of Groovy. Create two fields in the WordsToEnumTask
of type File and call them wordsFile
and
outDir
and add a bit of code to print the path of these files, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Then add a closure (a thing in curly brackets) to the end of the task create command that assigns values to them, like this:
1 2 3 4 5 6 7 8 |
|
Another uploadArchives
and project sync and when you run one of the wordsToEnum tasks you will see output a path to
the words list and a path to where our Java file will be written to. You can use this to check that you've put
the plugin_words.txt
file in the right place. :)
Step 4 - A little Gradle magic
Yes? What is it at the back? Can you speak up? Good question.
For those of you who didn't hear that, the question was "Won't this slow the build down if every time we compile our app this task will run to turn the word list into an Java class file?"
Gradle is clever enough to not do unnecessary work. If the output of a task or the input of a task haven't changed then it won't waste time by running the task again. We can designate the inputs and outputs of our task with more annotations.
Annotate the wordsFile
field in the WordsToEnumTask
with @InputFile
and the outDir
with @OutputDirectory
.
Yup. That's it. Simple huh?
If you really don't believe me add a simple println
statement into the makeWordsIntoEnums()
method.
You'll see, when all the code is complete, that you'll only see your output when you clean your
project or when you update the words list.
Step 5 - Let's do this thing
Time to write some code. In the WordsToEnumTask
write some really clever code to read the words
from the wordsFile
file and write a Java source file to the ourDir
.
I'm going to cheat and make use of the excellent JavaPoet by Square to
create the Java source file.
To do this add compile 'com.squareup:javapoet:1.5.1'
to the build.gradle
of your plugin. Hands up all
those to remembered to do a
refresh in your Gradle tool window? That was the big tip from the last post! (If you prefer to use IntelliJ
commands via CTRL/CMD + SHIFT + A, "Refresh all external projects" does the trick.)
Here's my complete task:
class WordsToEnumTask extends DefaultTask {
String group = "blogplugin"
String description = "Makes a list of words into an enum"
@InputFile
File wordsFile
@OutputDirectory
File outDir
@TaskAction
def makeWordsIntoEnums() {
Builder wordsEnumBuilder = enumBuilder("WordsEnum").addModifiers(Modifier.PUBLIC)
wordsFile.readLines().each {
wordsEnumBuilder.addEnumConstant(it).build()
}
TypeSpec wordsEnum = wordsEnumBuilder.build();
JavaFile javaFile = JavaFile.builder("com.afterecho.android.util", wordsEnum).build();
javaFile.writeTo(outDir)
}
}
Once again with the uploadArchive
and project sync, and when you run one of the wordsToEnum tasks...
well, once again we see very little. If you change your project view from Android to Project or browse
your project directory with your favourite file manager or shell, and look deep under app/build/generated/source/wordsToEnum/
you'll eventually find a WordsEnum.java
that will have an enum definition with all
of our words in.
We've turned something like this:
THIS
IS
A
JOURNEY
THROUGH
SPACE
into this:
public enum WordsEnum {
THIS,
IS,
A,
JOURNEY,
THROUGH,
SPACE
}
So we're done? Not quite.
Step 6 - The missing link
We have this nice enum but Android Studio doesn't know about it. Switch back to the Android view of
the project if you switched to Project view and try using WordsEnum
in one of your test app's source files. Android Studio will not
import the enum. Nor will it open the class if you use the Navigate Class keyboard shortcut.
What gives?
The missing thing is letting Android Studio know there's a new task that generates Java files and
where it places them. Back in the BlogPlugin
we need to add a single line within our loop:
variant.registerJavaGeneratingTask task, outputDir
Our complete BlogPlugin
class now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
You know the drill by now. uploadArchives
and sync your Android Studio project and run one of the wordsToEnum tas...but hold on a second!
Registering
our task as a Java Generating Task has has an interesting side effect: we no longer need to run the wordsToEnum task
by hand. It is now run as part of the standard assemble task. So whenever you run your app from within
Android Studio, or do a rebuild, or even build it from the command line our WordsEnum will be there.
So there you have it. A Gradle plugin that creates a Java source file that you can use in your Android app. In the next
installment we'll take some of that horrible hard-coded stuff, like the filename of the words list and the
package name of the enum, and move it into the build.gradle
of our Android project so it can be changed
without having to recompile the plugin each time.
I've uploaded the IntelliJ project for these posts to Github. Each part will be in its own branch. This one is at https://github.com/afterecho/gradle-plugin-tutorial/tree/part-two
Darren @ Æ
Comments
comments powered by Disqus