When working on a testing library in my spare time, I thought that annotations would be useful to create a graph structure the same way that Dagger does for object dependency, but I was only experienced with writing annotations that were referenced at run-time. 

So I went to the web for tutorials, blog posts, and videos on annotation processing. I was able to find enough information to set up my own annotation processor, but there wasn’t a comprehensive walkthrough on how to set it up for Android. Since annotation processing is purely Java, all the tutorials showed the processor in its own project, but I wanted my processor in the same project as my Android app so that a call to build the project would also trigger a build for the annotation processor; after all I needed this to be responsive to the tests I would create. So when I was asked what my first blog post would be, I was ready.

Annotations are a class of metadata that can be associated with classes, methods, fields, and even other annotations. This metadata can be accessed at runtime via reflection, but you can also access this metadata at build time via annotation processors. Annotation processors are a useful interface added in Java 6 that perform exhaustive searches over all annotations at build time and allow you to access reflective information regarding which element was annotated as well as any additional metadata stored in the corresponding annotation. Annotations and processors have not been deeply explored by mainstream developers until recently.

  • Jake Wharton gives a great in-depth presentation on annotation processing, its history, and its use in Dagger/Dagger2.
  • Hannes Dorfman gives a great presentation covering just the concepts of how annotation processing works.


Before we begin, let’s cover how we will be using annotation processing at a high level:

When you build and run this code, the first thing that happens is the annotation processor code is compiled into a jar via a pre-build gradle task so it can be automatically included and used in our Android project (annotation processors are pure Java modules as of RELEASE_8 and will not have access to the Android API). Next, the build process will begin processing all annotations in our Android project, including the custom one that we will write. Our annotation processor will create a generated java class object that can be used inside of our Android code at runtime. In doing so we will have proven the concept of generating code from annotations at build time and using the generated code during runtime.

The package structure is important and trying to rename or move packages to fix a mistake doesn’t always work as intended, so getting them right the first time makes life easier. The naming convention boils down to these two packages:

<base>              => com.stablekernel.annotationprocessor
<base>.processor    => com.stablekernel.annotationprocessor.processor

For this tutorial we will start with an Empty Activity default “Hello, world!” app created through the Android Studio wizard and I will have my Project pane set to Project.


1) Creating the processor module:
Since we are starting from an existing app, we will first need to create the annotation processor in its own module by either right clicking the project folder and selecting New>New Module or by going to File>New>New Module.

image00

Because this is not an Android native feature you will need to create a Java library module, not Android.

image01

image02

For this tutorial, the module will be named processor.
Make sure that your package name is correct: <base>.processor
The class name is simply the first file it generates in the library, I chose to name it for the annotation processor we will be making.


2) Setting the source compatibility:
In the build.gradle file for the in the app/ directory, set the android compile options.

image03

For this tutorial the compile options are:

compileOptions {
   sourceCompatibility JavaVersion.VERSION_1_7
   targetCompatibility JavaVersion.VERSION_1_7
}

image04

And in the processor module’s build.gradle file:

image05

Add the compatibility options:

sourceCompatibility = 1.7
targetCompatibility = 1.7

image06

Note that while the gradle file indicates that these arguments aren’t used, they are needed during the build process.


3) Creating the annotation:
Before we get to the processor, let’s create our CustomAnnotation.class as an annotation in this new module.

image07

image08

For now we will leave the auto-generated annotation empty as we only care about which elements are annotated in this tutorial.

image09


4) Creating the processor:
The processor class should extend from the AbstractProcessor class and be annotated with fully qualified paths of the annotation types that are expected to be handled (for this tutorial there is only the one) as well as the source version of Java. For this tutorial the source version is Java 7 but if your project is using Java 6 you would use RELEASE_6.

@SupportedAnnotationTypes(“<fully qualified annotation path>”)
@SupportedSourceVersion(SourceVersion.RELEASE_7)

image10

The easiest way to get the fully qualified path of the supported annotation type is to copy it using Android Studio’s project pane.

image11

It is important that you use a correct qualified path name and if you refactor the class later that you update this annotation otherwise the build will fail and the error tracing is not the easiest to follow.

image12

Android Studio should now be notifying you that you need to implement the process method, so let’s do that before moving on.

image13

Then replace the process method with the following:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   StringBuilder builder = new StringBuilder()
           .append("package com.stablekernel.annotationprocessor.generated;\n\n")
           .append("public class GeneratedClass {\n\n") // open class
           .append("\tpublic String getMessage() {\n") // open method
           .append("\t\treturn \"");


   // for each javax.lang.model.element.Element annotated with the CustomAnnotation
   for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
       String objectType = element.getSimpleName().toString();



       // this is appending to the return statement
       builder.append(objectType).append(" says hello!\\n");
   }


   builder.append("\";\n") // end return
           .append("\t}\n") // close method
           .append("}\n"); // close class



   try { // write the file
       JavaFileObject source = processingEnv.getFiler().createSourceFile("com.stablekernel.annotationprocessor.generated.GeneratedClass");


       Writer writer = source.openWriter();
       writer.write(builder.toString());
       writer.flush();
       writer.close();
   } catch (IOException e) {
       // Note: calling e.printStackTrace() will print IO errors
       // that occur from the file already existing after its first run, this is normal
   }


   return true;
}

To give a conceptual idea of what is happening here, the StringBuilder is creating a Java file with the package name in the generated namespace. This file is given a single method, getMessage which will return a string. That return value is being generated by finding each of the supported annotations and finding the name of the element associated with the annotation. In the case of this tutorial it will be MainActivity and onCreate as the two items annotated, so the generated file should look like this:

image14

Note that this is a generated file, it is created during the build process so you will not be able to view it until after the project has been built successfully. For reference you will find the file after a successful build in this directory: app/build/generated/source/apt/debug/<package>/GeneratedClass.java

Also, we are writing a source file here via Writer which serves our purpose for now but more complex file writing as your project develops may be made easier by third party libraries like JavaPoet.


5) Create the resource:
Now that we have created our processor we will need to tell Java to use it by creating the javax Processor file, this is what the compiler looks for to know how to handle the annotations. The steps are a bit tedious, but this is because the processor expects a specific structure to be in place.

  1. From the processor module’s main directory, create a new directory called resources
  2. Within this directory create a new directory called META-INF
  3. Within this directory create a new directory called services
  4. Within this directory create a new file called  javax.annotation.processing.Processor

image15

Inside this file you will put the fully qualified name of each of your processors, separated by a newline.They should autocomplete and in this instance we only have one so our file looks like this:

image16


6) Add android-apt:
Next, apply the android-apt plugin by first updating the build.gradle file for your project:

image17

And adding the buildscript dependency:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

image18

Then in the build.gradle file in the app/ directory:

image03

Apply the plugin:
apply plugin: 'com.neenbedankt.android-apt'

image19


7) Set build dependencies:
The main concept of this section is that we need to compile the processor and the annotation into a jar and then put that jar into our app module and make it available for reference, but this needs to be done before the app is built. First, we will update the dependencies to look for the jar file:

dependencies {
   compile files('libs/processor.jar')
   testCompile 'junit:junit:4.12'
   compile 'com.android.support:appcompat-v7:23.1.1'
}

Then we will create a gradle task that will move the jar file into the libs/ folder

task processorTask(type: Exec) {
   commandLine 'cp', '../processor/build/libs/processor.jar', 'libs/'
}

Finally, we establish the order of dependencies, so the :app:preBuild depends on our new processorTask, which depends on :processor:build:

processorTask.dependsOn(':processor:build')
preBuild.dependsOn(processorTask)

image20

So the build sequence is:

  1. :processor:build will generate the jar file with our annotation and its processor
  2. processorTask will copy this jar file to the android app/libs/ folder
  3. The :app module will now reference the new jar.

To verify that all of this happens as expected perform a clean build by either going to Build>Rebuild or in terminal executing

 

./gradlew :app:clean :app:build

and you should see the build process run in the correct order and finish with a processor.jar file in your app/libs/ folder.

image21


8) Apply annotations:
Now that the jar file containing the annotation and the annotation processor is in the android app we can reference the CustomAnnotation and can apply it to the MainActivity class declaration and the onCreate method.

image22


9) Verify annotations are working:
In order to verify that the annotations are being processed, we will launch an alert dialog by adding the following code to MainActivity.java and call it from onCreate

private void showAnnotationMessage() {
GeneratedClass generatedClass = new GeneratedClass();
String message = generatedClass.getMessage();
            // android.support.v7.app.AlertDialog
new AlertDialog.Builder(this)
.setPositiveButton("Ok", null)
.setTitle("Annotation Processor Messages")
.setMessage(message)
.show();
    }

But just applying these annotations will not create the generated class that the annotation processor is supposed to create, to do so we will need to rebuild again by going to Build>Rebuild or in terminal executing

 

./gradlew :app:clean :app:build

and now the generated file should be seen at this location:
app/build/generated/source/apt/debug/<package>/GeneratedClass.java

image23

image14


10) Running the build:
Building and running on a device now produces the following.

image25

This is the basis of annotation processing: by applying the @CustomAnnotation we are able to intercept it at build time, create a generated file, then at runtime this generated file is able to be used.


 

Are you an Android developer wanting to learn more? Check out "Level-up with Android Studio Shortcuts and Live Templates."

stable-kernel-mobile-app-development

About The Author

Chris Logan was formerly an Android developer at stable|kernel. He has worked in a polyglot capacity for several Atlanta-based companies including Tech, ranging from Web Development to OpenGL/CUDA to Chrome extensions. You can find out what he’s currently up to on his website.