Annotation Processing : Don’t Repeat Yourself, Generate Your Code.
Annotation processing is released in java 1.5. Actually it is an old API but one of the most powerful. We will talk about what is annotation processing, code generation and which libraries are already using custom annotation and generating code for us.
What is an annotation?
Actually we all know what it is. There are predefined annotation already in API. We use @Override annotation to override methods, @Singleton for using singleton pattern, bunch of android annotation like @NonNull, @StringRes, @IntRes etc. I am not going to deep dive into these annotation. I want to create new one.
Code Generation ?
Did you use @BindView for injecting views? Or did you inject into your classes by using @Inject. Or do you use Dagger library for dependency injection? We know that Dagger 2 uses annotations like @Component @Module or user defined scopes like @Fragment, @Activity.
Butterknife, Dagger2, Lombok, DeepLinkDispatch and lots of libraries generate code for us. Code generation is happening in compile time. The reason behind that is annotation processing takes place in compile time. And all annotations are build in javac(java compiler) for scanning and processing in compile time.
If we explains all annotation processing steps one by one;
1 — Build starts in java compiler. (java compiler knows all processors, So If we want to create new one, we need to tell to compiler about that.)
2 — Starts all Annotation Processors which is not executed.(Every processor has its own implementation)
3 — Loop over annotated elements inside the processor
3 — Finds annotated classes, methods, fields.
4 — Generate a new class with metadata of founded classes, methods, fields. (This is the place where you generate code.)
5 — Create new file and write your generated string as a class.
6 — Compiler checks if all annotation processors are executed. If not, start to next round.
I know one picture worth thousand words. Here is the following image.
How does ButteKnife Work?
1 — Define a view as global variable(Should not be private, it will be explained later.)
2 — Add annotation to view with it’s ID
3 — Bind your class to Butterknife.
When you click build. ButteKnife does all steps given above. And create an instance of generated class by reflection. And load your views.
Creating Custom Annotation in Android
I want to create a simple annotation that generates a navigator class and add newIntent methods for annotated classes. I will call it “Piri”. I know this is not a production library. Actually It is not even a library, just a sample project. But I want to give you main logic behind annotation processing.
I like to give steps before diving deep into topic. It is preparation for our understanding. We have 3 steps here.
1 — Create a new project : This is your app module.
2— Create an annotation module : This module is a java module. Only contains annotations.
3 —Create a processor module : This module is also java module and dependent to annotation module. This module does all calculation and code generation. You can use dozens of libraries here, you can use guava or something like that big. Why am I saying that? because this module is not packaged with your app. You app does not get fat with processor module and its dependencies. This module does all calculation and generation.Then finishes its job. We will show how it is possible later on.
Let’s start coding
Packages
piri-annotation module does not need any dependency. So there is no dependency in its piri-annotation/build.gradle file
piri-processor module needs piri-annotation module because processor will find annotated classes and does some calculation. As I said before, you can use any library in this processor module. This module will be kept out when your app package file is created. So this is piri-processor/build.gradle.
app module needs piri-annotation and piri-processor. But we don’t want piri-processor module in our .apk file. Here is the annotationProcessor tool comes to help. annotationProcessor means that we want that only only in compile time, don’t put it our .apk file.
Annotation
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface NewIntent {
}
@interface: This annotation tells to compiler this is the custom annotation.NewIntent is the name of our custom annotation.
@Target: What is your target? What do you want to annotate? Class or method? Constructor or field? Maybe you want to annotate another annotation? Here is the enum list that you can use as target. I copy-paste it from java.lang.annotation package.
public enum ElementType {
TYPE, //If you want to annotate class, interface, enum..
FIELD, //If you want to annotate field (includes enum constants)
METHOD, //If you want to annotate method
PARAMETER, //If you want to annotate parameter
CONSTRUCTOR, //If you want to annotate constructor
LOCAL_VARIABLE, //..
ANNOTATION_TYPE, //..
PACKAGE, //..
TYPE_PARAMETER, //..(java 8)
TYPE_USE; //..(java 8)
private ElementType() {
}
}
@Retention: annotation indicates how the custom annotation is stored. There are 3 types of retention.
SOURCE — analyses by compiler and never stored
CLASS — stored into class file and not retained in runtime
RUNTIME — store into class file and usable in runtime(by reflection)
Processor
This is the where magic happens. You spend most of time here If you want to do code generation.
public class NewIntentProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {}
@Override
public Set<String> getSupportedAnnotationTypes() {}
@Override
public SourceVersion getSupportedSourceVersion() {}
}
init(): gives you paintbrushes to start painting. Filer(to generate file), Messager(debugging), Utility classes. You can get these classes with processing environment.
process(): brain of your processor. Starts rounding and gives you annotated classes, methods, fields, annotation etc. It gives you all annotated elements here. And you start doing all calculation and generate your new class file here.
getSupportedAnnotationTypes(): We return only our custom annotation set in this method. We can say that return value of this method will be given to us as process method’s first parameter.
getSupportedSourceVersion(): We always return latest java version.
Let’s Process
Three steps in process() method.
1 — Find all annotated element
Element is a parent interface for all elements. First of all we check if our annotation is used in class or not. If it is not annotated in class then we print error message and return. If there is no error, this means that we can safely cast element to TypeElement. TypeElement is a subinterface which is extends from Element. And we use TypeElement for classes, method parameters. There are some other subinterfaces extends from Element.
package com.example; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
2 — Create class and methods (Using JavaPoet library)
You should use JavaPoet If you want to do code generation. It is so easy to generate classes, methods, parameters. I think no need to explain more, everything is so clear in this library.
3 — Write it to a source file.
At last, write to a file. Lets bring code snippets together. Here.
This processor will generate us following output class. This is the generated class.
Let’s use our new annotation in our app module.
@NewIntent
public class MainActivity extends AppCompatActivity {}
And call generated class and its method.
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
Navigator.startMainActivity(this); //generated class, method
}
}
Congrats. It is all done!
Thanks for reading. Please don’t hesitate to comment. And don’t forget to recommend :)