In this post I'm going to explore the world of the transform api, as suggested by Reddit user sahal2080 in this comment
This is the forth part of an undecided number of posts about creating a Gradle plugin for Android. Part one can be found here, Part two can be found here and Part three can be found here.
Let me get something straight right from the outset; this is not going to be a post about how to manipulate Java class files. Why? Well essentially because I don't know how to. What I'm doing here is introducing the hook by which you can do your own manipulation.
Also an admission. Before I started writing this post I had no clue about the transform api. What you get here is just what I've worked out from faffing about with it. If it works for you...great! If Chet Haase lambasts me and tells you all not to ever, ever do it this way...listen to him.
As in the previous parts, I'm going to make some assumptions. For one I'm going to assume you've read the previous parts. At the very least you need to have the part 3 code open in IntelliJ.
Step 0 - Where it begins
The first bit of trouble I had with the transform api is there's no where
that tells you where the bloody thing is. With some guessing and a little
investigation of jcenter the magic extra line
to add in to the dependencies section of the build.gradle
of the plugin is
compile 'com.android.tools.build:transform-api:1.5.0'
Add this in and remember to sync your project.
Perhaps this is a safety thing? Maybe they assume that if you can't find this out for yourself then you shouldn't be even trying this stuff? Who knows? Maybe I'm just not looking in the right places for the documentation.
Step 1 - Transformers - classes in disguise
The transform api page states
To insert a transform into a build, you simply create a new class implementing one of the Transform interfaces, and register it with android.registerTransform(theTransform) or android.registerTransform(theTransform, dependencies).
Looking at the javadoc
Transform
is an abstract class, not an interface. So create a new groovy class
in the plugin project called BlogTransform
and make it extend Transform
. There
are a few Transform classes and the one you want is com.android.build.api.transform.Transform
Use IntelliJ to implement the missing methods and you'll end up with something
like this:
class BlogTransform extends Transform {
@Override
String getName() {
return null
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return null
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return null
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
}
Change the implementation of getName()
to return a nice name for the transform.
In our case as the transform will do nothing of consequence let's make it return
the string "BlogDoNothing".
The getInputTypes()
should return the InputTypes that the transform will work
on. There are two defined; QualifiedContent.DefaultContentType.CLASSES
and
QualifiedContent.DefaultContentType.RESOURCES
The documentation says that the former "is compiled Java code" and the latter is "standard Java resources". For this example we just need the former, so make the method return
Collections.singleton(QualifiedContent.DefaultContentType.CLASSES)
Similarly there are a number of scopes, such as external libraries, sub-projects,
tested code, etc. For this example we are only concerned with our own project
so make getScopes()
return
Collections.singleton(QualifiedContent.Scope.PROJECT)
To make things simple we will leave inIncremental()
returning false. If this
is changed then the transform can be called when only part of the build needs
updating.
Step 2 - Wiring it up
Before going in to the actual guts and implementing the transform()
method,
quickly pop back to the BlogPlugin
class. To actually register our transformer
we add one line to the apply()
method:
target.android.registerTransform(new BlogTransform())
Don't put this in the target.afterEvaulate
closure. The transform is called
for each variant automatically, so there's no need to register a transform per
variant.
Step 3 - Setting up
The first parameter of the transform()
method is a Context
. But this isn't
the Context that we all know and love from our beloved Android. This one only
has three methods, and for this example don't care about any of them. There's
a way to get hold of the LoggingManager
, a File
object to a directory where
we can write temporary files, and the "path" to the name of the task.
Skipping over inputs
for a second, we are also not interested in the referencedInputs
.
If you override the getReferencedScopes()
method you can get something here
which you might want to refer to for your transforming, but you won't be transforming
yourself. Too complicated for us at this point.
Jumping straight to the last parameter - isIncremental
- as mentioned above,
we are keeping it simple and not concerning ourselves with incremental builds
right now.
So that leaves us with two: inputs
and outputProvider
.
inputs
is, quite simply, the thing or things we want to transform. It's
a Collection of TransformInput
. Each TransformInput
has a Collection of
DirectoryInput
and JarInput
. In our trivial case, there appears to only
be one TransformInput
with one DirectoryInput
so that makes it easy.
Calling getFile
on the DirectoryInput
returns a File
object on the directory
that holds the compiled class files for our project, which is something like
build/intermediates/classes/debug
for the debug variant. From here we can traverse the directory structure to
access all of our class files.
(As an aside, if we were to implement "incremental" then DirectoryInput
has
a method - getChangedFiles()
which returns us a list of files that have
been changed. We can use this to only transform the files that have been
updated to save time.)
We don't modify the class files in place. Instead we ask the outputProvider
for the location to write our transformed class files. This is done by calling
getContentLocation
.
The first parameter is a name. As the javadoc says, "For a given set of scopes/types/format it must be unique." We can just put in a simple string here, such as "blogdonothing" as we're only looking at one scope, one type, one format. (Hmm... an idea for a song lyric there.)
The next two parameters we can get from our implementations of getInputTypes
and getScopes
.
The last parameter determines how our transformed output is stored. There's a choice of two; directory or jar. Let's choose directory.
Step 4 - Do the transform
Now we're ready to go. When transform
is called we will have all the compiled
class files for our project at our disposal. When we end transform
only
the classes that are in the output directory will get bundled up into the
output APK file. That bears repeating; if you are not transforming a class
file you still need to copy it into the output location.
I didn't realise this initially and when I investigated my APK with ClassyShark all of my classes were missing.
For fun let's pretend we're a very simple proguard and we want to strip out the
BuildConfig
class from our APK. Here is the full BlogTransform
class.
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
It's not very elegant, but it shows the idea. We start by determining an output directory (line 26) using all the info I mentioned in step 3 (name, type, etc.). Then we delete it and recreate it to make sure there's nothing in it from the last compile (lines 28 and 29).
Next it's time to iterate over the directoryInputs in the inputs (lines 31 and 32). We traverse the directory tree (line 34) and strip off the start of the path to leave us with just the bit from the package name (e.g. com/afterecho/...) (line 35).
If this is a directory then create the same directory in the output location (lines 36 and 37).
If this is a file then copy it as long as it's not called BuildConfig.class (lines 39 and 40).
Step 5 - Wrapping it up
Perform an uploadArchives
task and then sync your AndroidStudio project. In
the Gradle projects tool window, under "other" there will now be three new
tasks: transformClasesWithBlogDoNothingForDebug
, transformClasesWithBlogDoNothingForRelease
and transformClasesWithBlogDoNothingForDebugAndroidTest
Do an "assembleDebug" and then, from the Project view, look in the
app/build/intermediates/transforms
directory for a BlogDoNothing
. If you
dive in there you'll find your class files...minus BuildConfig.class
as
expected. (The originals are in app/build/intermediates/classes
if you want
to prove that BuildConfig
really does exist.)
If you use ClassyShark to open app/build/outputs/apk/app-debug.apk
you can
really see that the BuildConfig
class is gone. Try changing the condition in
the plugin to remove a different class. After you uploadArchives
and sync,
clean your project and assembleDebug
again to see that BuildConfig
comes
back and whatever class you excluded gets removed. It's important to clean
your project if you haven't made any changes to your code as Gradle, helpful as
ever, will not call your transform unless something has changed.
So there you have it. A very trivial implementation of the transform api for you to build on. An important thing to note is that although you could add in new classes, methods, etc. at this stage, IntelliJ will not be aware of them (unlike the Words enum from previous posts). So you would only be able to access them through something like reflection. I guess you could have a dummy or stub implementation of a class in your code and replace it with a fully implemented version in a transform during the package assembly. The gradle retrolambda plugin uses the transform to do it's work, replacing the Java 8 bytecode for lambdas etc. with Java 6 or 7 compatible bytecode.
Feel free to leave any comments or let me know if you use what I've written here as the springboard to some wonderful gradle plugin of your own.
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-four
Darren @ Æ
Comments
comments powered by Disqus