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.

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

 

Annotation Processing in Android Studio

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.

Annotation Processing in Android Studio

For this tutorial the compile options are:

compileOptions {
   sourceCompatibility JavaVersion.VERSION_1_7
   targetCompatibility JavaVersion.VERSION_1_7
}

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

Add the compatibility options:

sourceCompatibility = 1.7
targetCompatibility = 1.7

Annotation Processing in Android Studio

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.

Annotation Processing in Android Studio

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

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)

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

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.

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

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;nn")
           .append("public class GeneratedClass {nn") // open class
           .append("tpublic String getMessage() {n") // open method
           .append("ttreturn "");


   // 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:

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

 

6) Add android-apt:

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

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

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)

Annotation Processing in Android Studio

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.

Annotation Processing in Android Studio

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.

Annotation Processing in Android Studio

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

Annotation Processing in Android Studio

 

Annotation Processing in Android Studio

10) Running the build:

Building and running on a device now produces the following.

Annotation Processing in Android Studio

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.

  1. Hi, Thank you so much for your great a I have done an annotation processor based library by following your tutorial, I am struggling to publish my annotation based library. Whereas people can integrate the library easily how we can use butterknife. Could you tell me how I can publish annotation based library .

Leave a Reply to Pavan Hugar Cancel reply

Your email address will not be published. Required fields are marked *