Android Annotation Processing Tutorial: Part 2: The Project Structure

cover-android-annotation-processing-tutorial-part-2

In this tutorial, we will build a project as a complete library implementation. The reader will be able to develop his own library similar to ButterKnife, Room etc.

The source code for this tutorial can be found here: https://github.com/MindorksOpenSource/annotation-processing-example

Link to other parts of the tutorial:

  1. Part1: A practical approach
  2. Part3: Generate Java source code
  3. Part4: Use the generated code

Let's get started

We will develop a project that will be similar to the ButterKnife but very minimalistic. It will be written in the form of an Android library.

Following are the list of features that we will develop:

  1. @BindView to map XML view to the variable using the view's id.
  2. @OnClick to map a method to a view's OnClickListerner
  3. Generate binding classes to deal with mappings and casts

Example client code:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_content)
    TextView tvContent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Binding.bind(this);
    }

    @OnClick(R.id.bt_1)
    void bt1Click(View v) {
        tvContent.setText("Button 1 Clicked");
    }

    @OnClick(R.id.bt_2)
    void bt2Click(View v) {
        tvContent.setText("Button 2 Clicked");
    }
}

The source code for this tutorial can be found here

https://github.com/MindorksOpenSource/annotation-processing-example

There are four modules in this project:

  1. app : This is the Android app project.
  2. binder: This module provides a class that maps a given activity's annotated view objects and click listener methods to the XML views.
  3. binder-annotations: This module defines the annotations to facilitate the mapping of the views and click listeners.
  4. binder-compiler: This module defines the processor that generates the classes to help the above mappings.

We will first define annotations

Annotations are the metadata that can be added to a Java source file. They help in reading the properties of a class and its members while processing.

The annotations will be used in the Android application as well as the Processor, so we will create a Java library named binder-annotations.

Android Studio -> file -> new module -> java library

We will define three annotations:

1. BindView: It will map a view reference to its XML definition. Example: TextView with id tv_content will be mapped to a variable tvContent.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}

2. OnClick: It will map a method which will be called when a view with the provided id is clicked.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface OnClick {
    @IdRes int value();
}

3. Keep: This is an interesting annotation. We have to create this annotation to prevent the proguard to obfuscate our generated classes. You will understand the importance of this annotation later in the tutorial.

/**
 * It will keep any class after proguard minify
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Keep {
}

In this definition there are three things to understand:

Note: We define an annotation using the @interface keyword.

  1. RetentionPolicy: It defines the level of presence of the annotation in the code. Here SOURCE implies that the annotation will be present in the Java source code but will be removed when the code is compiled. CLASS policy means that the annotations will be preserved in the compiled bytecode in addition to the source code.
  2. Target: It defines the elements on which the annotation can be used. FIELD means that the annotations can be used on a member variable. METHOD means that it can be used on any class methods and TYPE means that is can be used on a class.
  3. Value: It defines the type of primitive value that can be passed through these annotations. Here we can pass an int value to the annotations.

Here you can notice @IdRes annotation. This annotation is provided by the support-annotations library. We have used this so that the IDE can show the error if any int value other than the resource id corresponding to the view is passed as the annotation's value. To achieve this we have to add the following dependency in the binder-compiler module.

compileOnly 'com.android.support:support-annotations:27.1.1'

CompileOnly allows us to use a library in a module but not make it available when it is distributed. Since all the android application inculde this dependency so we are safe to not ship this dependency will our library distribution.

Now that we have defined our annotations. We will move to the fun part of this tutorial i.e. creating the processor that does the magic.

Slow but steady wins the race

Create Annotation Processor

Annotation processor run in cycles and in parallel to the application compilation. In each cycle, the processor is provided with the information about the application's source code being compiled.

A processor must be registered to the compiler so that it can run while the application is being compiled. We will see how to define such a compiler.

Now, we will create a Java library binder-compiler similar to our binder-annotations. In this module we will have to create the directory structure:

binder-compiler/src/main/resources/META-INF/services

In the services directory, we will create a file named javax.annotation.processing.Processor. This file will list the classes that the compiler will call when it compiles the application's source code while annotation processing.

src/main/resources/META-INF/services/javax.annotation.processing.Processor path is the defined path for Java compiler to look for annotation processors.

Annotation Processor Definition

All annotation processor inherit AbstractProcessor which defines the base methods for the processing. We will create a class Processor in this library that inherit AbstractProcessor. We have to override three methods to provide our implementations for the processing.

  1. init: Here we will get Filer, Messager, and Elements.
  2. process: This method is called to process the source code of the application. Here we will define a class and write the Java source code.
  3. getSupportedAnnotationTypes: It lists the annotations that we intend to query while processing the application's Java files.

Filer, Messager, Elements:

  1. Filer: It provides the APIs to write the generated source code file.
  2. Messager: It is used to print messages when the compilation is taking place. We send the error messages that may arrises in processing via Messager. Since annotation processor runs in its own separate environment, we can not communicate with the application by any other means.
  3. Elements: It provides the utils methods for filtering the different type of elements in the processor.
public class Processor extends AbstractProcessor {

    private Filer filer;
    private Messager messager;
    private Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // all the magic happens in this block  
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return new TreeSet<>(Arrays.asList(
                BindView.class.getCanonicalName(),
                OnClick.class.getCanonicalName(),
                Keep.class.getCanonicalName()));
    }
}
We need to register this Processor to the javax.annotation.processing.Processor file using its complete cannonical name.
com.mindorks.compiler.lib.Processor

Now before we proceed forward we must understand the Element.

Element

Element represents a program element such as package, class, or methods. We work with elements to read its properties while processing. Three important elements that we need for this tutorial are:

  1. TypeElement: It represents a class or interface.
  2. VariableElement: It represents a field.
  3. ExecutableElement: It represents a method.

We will continue this tutorial in PART 3

In the next part of this tutorial, we will provide the complete implementation of the Processor's process method and learn to define a class and its members using JavaPoet.

Thanks for reading this article. Be sure to share this article if you found it helpful. It would let others get this article and spread the knowledge.

Let’s become friends on Twitter, Linkedin, Github, and Facebook.

Learning is a journey, let’s learn together!