Skip to content

hitanshu-dhawan/AnnotationProcessing

Repository files navigation

Full article here: https://medium.com/androidiots/writing-your-own-annotation-processors-in-android-1fa0cd96ef11

Android Weekly

What are Annotations ?

An annotation is a form of syntactic metadata that can be added to Java source code.
We can annotate classes, interfaces, methods, variables, parameters etc.
Java annotations can be read from source files. Java annotations can also be embedded in and read from class files generated by the compiler.
Annotations can be retained by Java VM at run-time and read via reflection.

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

Creating an annotation requires two pieces of information: Retention and Target

A RetentionPolicy specifies how long, in terms of the program lifecycle, the annotation should be retained for. For example, annotations may be retained during compile-time or runtime, depending on the retention policy associated with the annotation.

The Target of an annotation specifies which Java ElementTypes an annotation can be applied to.

Why Annotation Processors ?

1. Runtime Compile time

2. No Reflection

3. Generate boilerplate code*

*Annotation processing can only be used to generate new files and not to modify existing ones

How does Annotation Processing work ?

The annotation processing takes place in many rounds. The compiler reads a java source file with the registered annotations and calls their corresponding annotation processors which will generate more java source files with more annotations. These new annotations will again call their corresponding annotation processors which will again generate more java source files. This cycle continues until no new java source file is generated in the cycle.

How to register a Processor ?

A processor must be registered to the compiler so that it can run while the application is being compiled.

Annotation Processors can be registered in two ways.

1.

Create a directory structure like this
<your-annotation-processor-module>/src/main/resources/META-INF/services
Now, in the services directory, we will create a file named javax.annotation.processing.Processor.
This file will list the classes (fully qualified name) that the compiler will call when it compiles the application's source code while annotation processing.

2.

Use Google's AutoService library.
Just annotate your Processor with @AutoService(Processor.class)

Example:

package foo.bar;

import javax.annotation.processing.Processor;

@AutoService(Processor.class)
final class MyProcessor implements Processor {
  // …
}

How to create a Processor ?

To create our custom Annotation Processor we need to make a class that extends AbstractProcessor which defines the base methods for the processing.
We have to override four methods to provide our implementations for the processing.

public class Processor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // initialize helper/utility classes...
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        // do processing...
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return new HashSet<String>() {{
            add(BindView.class.getCanonicalName());
            add(OnClick.class.getCanonicalName());
        }};
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

init() gives you Helper/Utility classes like Filer (to generate files), Messager (for logging errors, warnings etc.), Elements (utility methods for operating on program elements), Types (utility methods for operating on types) etc.
You can get these classes with processingEnvironment.

process() is the method where all the processing happens.
It gives you annotations that need to be processed and roundEnvironment provides the information about the round and has some utility methods for querying elements.
e.g. processingOver(),getRootElements(),getElementsAnnotatedWith() etc.

getSupportedAnnotationTypes() returns the names of the annotation types supported by this processor.

getSupportedSourceVersion() returns the latest source version supported by this processor.

Note : You can also use @SupportedAnnotationTypes and @SupportedSourceVersion instead of getSupportedAnnotationTypes() and getSupportedSourceVersion() respectively.

Demo #1 : Singleton

  1. Singleton.java
  2. Processor.java

Demo #2 : KSingleton

  1. KSingleton.kt
  2. Processor.kt

Important Classes/Objects

ProcessingEnvironment : An annotation processing tool framework will provide an annotation processor with an object implementing this interface so the processor can use facilities provided by the framework to write new files, report error messages, and find other utilities.

Elements : Utility methods for operating on program elements. Can be accessed by ProcessingEnvironment.getElementUtils().

Types : Utility methods for operating on types. Can be accessed by ProcessingEnvironment.getTypeUtils().

Messager : A Messager provides the way for an annotation processor to report error messages, warnings, and other notices. Can be accessed by ProcessingEnvironment.getMessager().

Filer : This interface supports the creation of new files by an annotation processor. Can be accessed by ProcessingEnvironment.getFiler().


RoundEnvironment : An annotation processing tool framework will provide an annotation processor with an object implementing this interface so that the processor can query for information about a round of annotation processing. We can get our desired elements with RoundEnvironment.getRootElements() and RoundEnvironment.getElementsAnnotatedWith() methods.


ElementFilter : Filters for selecting just the elements of interest from a collection of elements. Contains methods like ElementFilter.constructorsIn(), ElementFilter.methodsIn(), ElementFilter.fieldsIn() etc.

How to generate .java files ?

We can use Square's JavaPoet library for generating .java files.
JavaPoet makes it really simple to define a class structure and write it while processing. It creates classes that are very close to a handwritten code.

Example

This HelloWorld class

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

can be generated by this piece of code

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

Note : There's also another Square library KotlinPoet for generating .kt files.

Demo #3 : ButterKnife

Annotations

  1. BindView.java
  2. OnClick.java

Processor

  1. Processor.java

Library

  1. ButterKnife.java

Example

This ButterKnife library will generate separate classes (with suffix Binder) for each of the Activity where we used the annotations @BindView or @OnClick.

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private int numberOfTimesTextViewClicked = 0;

    @BindView(R.id.text_view)
    TextView textView;

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

        ButterKnife.bind(this);
    }

    @OnClick(R.id.text_view)
    void onTextViewClicked(View view) {
        textView.setText(String.valueOf(++numberOfTimesTextViewClicked));
    }
}

MainActivityBinder.java

public class MainActivityBinder {
    public MainActivityBinder(MainActivity activity) {
        bindViews(activity);
        bindOnClicks(activity);
    }

    private void bindViews(MainActivity activity) {
        activity.textView = (TextView) activity.findViewById(2131165314);
    }

    private void bindOnClicks(final MainActivity activity) {
        activity.findViewById(2131165314).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                activity.onTextViewClicked(view);
            }
        });
    }
}

References