This is the third part of an undecided number of posts about creating a Gradle plugin for Android. Part one can be found here and Part two can be found here.
For those of you that are asking "Part 2? I've been on tenterhooks waiting for part 2 and you're telling me it's been out for ages and I missed it? Why didn't you tell me!??!" Well I managed to choose the day that Google released the 23.2 version of the Support Library to post it, so people were a bit pre-occupied. Ho hum! At least I didn't choose the day that the 'N' preview was released to post part 3.
At the end of the previous part I alluded - well virtually promised - that the next part would deal with taking the hard-coded stuff out of the code and into the build.gradle file. So I'd better keep that promise.
As before to make the code listings below a little shorter and more concise I haven't included the package definition or the imports. And I'm going to assume that you've done parts one and two, or at least read them.
Step 0 - Three little words
There are three things in the plugin that are hard-coded at the moment that could do with being less hard; the filename of the words, the name of the enum class, and the package that the enum class goes into.
Let's pull them out into our build.gradle
into a section called bpconfig
.
Add the following to the build.gradle
of your test app that is using the
plugin:
bpplugin {
words "plugin_words.txt"
enumClass "WordsEnum"
outputPackage "com.afterecho.android.util"
}
Android Studio will prompt you to re-sync your project after making the change.
Try it. Aaanndd... whoops! The sync fails, complaining about bpplugin not
being found. You see you can't put just any old thing in the build.gradle
.
Step 1 - Building an extension
Back to IntelliJ and create a new Groovy class BlogPluginExtension
. This
class needs to have just the three fields to correspond to the three config
items we have. All in all it will look like
class BlogPluginExtension {
String words
String enumClass
String outputPackage
}
To hook it into our plugin, we create an extension. In the BlogPlugin
class
add this as the first line of the apply()
method.
target.extensions.create('bpplugin', BlogPluginExtension)
Surprisingly enough this creates an extension block called bpplugin within
the project and Gragle maps the values from the build.gradle
into the fields
of BlogPluginExtension
. Neat huh?
A quick uploadArchives and now, when you sync your test project, there are no complaints. We have successfully added an extension block. But we're not actually using it yet.
Step 2 - Making use of it
After the target.extensions.create
line
you added to the apply()
method, add some debug lines
println target.extensions.bpplugin.words
println target.extensions.bpplugin.enumClass
println target.extensions.bpplugin.outputPackage
Do the uploadArchives and sync dance and observe the output in the Gradle console windows. Do you see what I see? "null", "null" and "null". Hmmm. Why is that?
At this point I defer to the magnificent Dan Lew and his post Lessons learned from our first Gradle plugin for Android, Victor and the section Evaluating Extensions
Excellent. Thanks Dan. So we sort this out by simply wrapping almost everything
in our apply()
method in an afterEvaluate
so it looks a little something like
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
(Note that the showDevices task also got moved into the afterEvaluate block. As this task doesn't have any config there's no need for it to. If you feel that the world would be a brighter and happier place if it was not in afterEvaluate then please feel free to move it.)
After a quick uploadArchives and sync and those pesky nulls have vanished. From here it's a quick
job to replace the hard-coded filename with the one from the extension in the apply()
method.
And the class and package name in the plugin? Well remember from part 2 where we moved the
adb devices task into it's own class? We can access the Project via the project
property
of our Task, so we just replace the hard-coded values in there too.
So we end up with our Plugin looking like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
And our Task looking like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Step 4 - A little gotcha
After all this you would expect that you can change the contents of the bpplugin
block in your test project's
build.gradle
and it would take effect when you re-sync the project right? Well actually it's not quite that
simple.
Recalling part 2, I said
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.
Well it turns out that the configuration block in the build.gradle
doesn't automatically count toward an input,
so changing the values in the configuration won't make Gradle rebuild the output. You can do a clean
and then
the change will take effect. But I think we can do better than that, don't you?
Instead of pulling the enumClass
and outputPackage
from the project within the Task, let's pass them
in from the Plugin instead. Create two new fields in the Task - enumClassName and outputPackageName
and annotate them with @Input
. This tells Gradle to consider these as inputs when deciding whether
or not to run the task. Change the code to use these fields so the Task now looks like
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Modify the Plugin to pass those values in, the same way as we do for the words filename and output directory, so it looks like
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Now you can change the values in the bpplugin block of the build.gradle
and the change will take
effect as soon as you sync the project.
There is one more little gotcha about this though. Changing the inputs doesn't clear out the old
outputs. Try changing the enumClass
to something else in the build.gradle
and syncing your project. If you look
in the build/generated/source/wordsToEnum
folder you'll find two .java files; one with the
new name you set and one with the old name. The simple way
to deal with this is to clean your project and rebuild if you change anything in the build.gradle
.
Or you could put in a outDir.deleteDir()
at the start of your task. :)
So that's it. Another step on the road to Gradle awesomeness. I'd like to thank insomnia, without which this post might not have gotten written until the sun was up, and coffee, without which there would probably be a lot more (or a lot less) typos. Please leave comments - either below or on my Twitter feed - especially if there's anything that makes no sense, is blatantly wrong, or you'd like covered in a later blog post.
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-three
Darren @ Æ
Comments
comments powered by Disqus