A beginner's guide to annotation processing.
In this talk that I gave at Droidcon Tel Aviv in 2016, I walk you through the process of building a custom annotation processor which mimics some of the behavior you may be familiar with from the popular Android library: Butter Knife.
Top 5 Benefits OF Using Muvi Live Paywall For Live Streams
Beginner's guide to annotation processing
1. Write code that writes code!
A beginner’s guide to annotation processing.
2. Obligatory Speaker Details
• Software Engineer for Bandcamp
• I’m from the US, but am living in Europe for now.
(working remotely)
• I have a dog named Watson. On weekends, we
walk across the Netherlands together.
3. Questions we ask ourselves in the
beginning.
• What is an annotation, and what is annotation
processing?
• Why would I want to process annotations?
• How do I make something cool? Maybe a
ButterKnife clone?
6. Annotations
• You’ve seen them before (e.g. @Override, @Deprecated, etc.)
• They allow you to decorate code with information about the
code (ie: they are meta data)
• Kind of like comments, but they are more machine
readable than human readable.
• Annotations can be used by the JDK, third party libraries, or
custom tools.
• You can create your own annotations.
13. Annotation Processors
• Operate at build-time, rather than run-time.
• Are executed by the “annotation processing
tool” (apt)
• Must be part of a plain-old java library, without
direct dependencies on Android-specific stuff.
• Extend from
javax.annotation.processing.AbstractProcessor
15. Annotation Processing
* If a processor was asked to process on a given round, it will be asked to process on
subsequent rounds, including the last round, even if there are no annotations for it to
process.
https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html
List unprocessed
source files with
annotations.
Register
Annotation
Processors
Any
Processors
for them?
Run ProcessorsCompile
No* Yes
16. Why would you want to make one?
• Boilerplate Reduction
• Reducing Boilerplate
• Reduced Boilerplate
• ….
• It’s pretty cool.
18. “Soup Ladle”
• Wanted something that sounded like Butter
Knife, but was a different utensil.
• I like Soup.
• Ladles are big spoons.
• Big spoon = more soup in my face at once.
19. Soup Ladle Goals
• Allow for view binding with an annotation:
@Bind(R.id.some_id) View fieldName;
• Perform the binding easily using a one liner in
onCreate:
SoupLadle.bind(this);
• That’s it.. we are reinventing the wheel for
learning’s sake and don’t need to go all in.
20. Approach
1. Define the @Bind annotation.
2. Extend AbstractProcessor to create our
annotation processor for @Bind.
3. Within our processor: scan for all fields with
@Bind, keeping track of their parent classes.
4. Generate SoupLadle.java with .bind methods
for each parent class containing bound fields.
21. Approach
1. Define the @Bind annotation.
2. Extend AbstractProcessor to create our
annotation processor for @Bind.
3. Within our processor: scan for all fields with
@Bind, keeping track of their parent classes.
4. Generate SoupLadle.java with .bind methods
for each parent class containing bound fields.
27. Approach
1. Define the @Bind annotation.
2. Extend AbstractProcessor to create our
annotation processor for @Bind.
3. Within our processor: scan for all fields with
@Bind, keeping track of their parent classes.
4. Generate SoupLadle.java with .bind methods
for each parent class containing bound fields.
28. Extending AbstractProcessor
public class AnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> result = new HashSet<>();
result.add(Bind.class.getCanonicalName());
return result;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// logic goes here
// as does the actual code generation
//
// we’ll get to this stuff in a little bit
}
}
29. Extending AbstractProcessor
public class AnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> result = new HashSet<>();
result.add(Bind.class.getCanonicalName());
return result;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// logic goes here
// as does the actual code generation
//
// we’ll get to this stuff in a little bit
}
}
30. Extending AbstractProcessor
public class AnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> result = new HashSet<>();
result.add(Bind.class.getCanonicalName());
return result;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// logic goes here
// as does the actual code generation
//
// we’ll get to this stuff in a little bit
}
}
31. Extending AbstractProcessor
public class AnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> result = new HashSet<>();
result.add(Bind.class.getCanonicalName());
return result;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// logic goes here
// as does the actual code generation
//
// we’ll get to this stuff in a little bit
}
}
32. Extending AbstractProcessor
public class AnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> result = new HashSet<>();
result.add(Bind.class.getCanonicalName());
return result;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// logic goes here
// as does the actual code generation
//
// we’ll get to this stuff in a little bit
}
}
33. Extending AbstractProcessor
public class AnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> result = new HashSet<>();
result.add(Bind.class.getCanonicalName());
return result;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// logic goes here
// as does the actual code generation
//
// we’ll get to this stuff in a little bit
}
}
34. Extending AbstractProcessor
public class AnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> result = new HashSet<>();
result.add(Bind.class.getCanonicalName());
return result;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// logic goes here
// as does the actual code generation
//
// we’ll get to this stuff in a little bit
}
}
35. Approach
1. Define the @Bind annotation.
2. Extend AbstractProcessor to create our
annotation processor for @Bind.
3. Within our processor: scan for all fields with
@Bind, keeping track of their parent classes.
4. Generate SoupLadle.java with .bind methods
for each parent class containing bound fields.
36. Processing…
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) {
return true;
}
Map<TypeElement, List<VariableElement>> bindingClasses = new HashMap<>();
for (Element e : roundEnv.getElementsAnnotatedWith(Bind.class)) {
VariableElement variable = (VariableElement) e;
TypeElement parent = (TypeElement) variable.getEnclosingElement();
List<VariableElement> members;
if (bindingClasses.containsKey(parentClass)) {
members = bindingClasses.get(parentClass);
} else {
members = new ArrayList<>();
bindingClasses.put(parentClass, members);
}
members.add(variable);
}
// .. generate code ..
}
37. Processing…
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) {
return true;
}
Map<TypeElement, List<VariableElement>> bindingClasses = new HashMap<>();
for (Element e : roundEnv.getElementsAnnotatedWith(Bind.class)) {
VariableElement variable = (VariableElement) e;
TypeElement parent = (TypeElement) variable.getEnclosingElement();
List<VariableElement> members;
if (bindingClasses.containsKey(parentClass)) {
members = bindingClasses.get(parentClass);
} else {
members = new ArrayList<>();
bindingClasses.put(parentClass, members);
}
members.add(variable);
}
// .. generate code ..
}
38. Processing…
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) {
return true;
}
Map<TypeElement, List<VariableElement>> bindingClasses = new HashMap<>();
for (Element e : roundEnv.getElementsAnnotatedWith(Bind.class)) {
VariableElement variable = (VariableElement) e;
TypeElement parent = (TypeElement) variable.getEnclosingElement();
List<VariableElement> members;
if (bindingClasses.containsKey(parentClass)) {
members = bindingClasses.get(parentClass);
} else {
members = new ArrayList<>();
bindingClasses.put(parentClass, members);
}
members.add(variable);
}
// .. generate code ..
}
39. Processing…
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) {
return true;
}
Map<TypeElement, List<VariableElement>> bindingClasses = new HashMap<>();
for (Element e : roundEnv.getElementsAnnotatedWith(Bind.class)) {
VariableElement variable = (VariableElement) e;
TypeElement parent = (TypeElement) variable.getEnclosingElement();
List<VariableElement> members;
if (bindingClasses.containsKey(parentClass)) {
members = bindingClasses.get(parentClass);
} else {
members = new ArrayList<>();
bindingClasses.put(parentClass, members);
}
members.add(variable);
}
// .. generate code ..
}
40. Approach
1. Define the @Bind annotation.
2. Extend AbstractProcessor to create our
annotation processor for @Bind.
3. Within our processor: scan for all fields with
@Bind, keeping track of their parent classes.
4. Generate SoupLadle.java with .bind methods
for each parent class containing bound fields.
49. JavaPoet
• Builder-pattern approach to programmatically
defining a class and its fields/methods.
• Automatically manages the classes needed for
import.
• When you’re ready, it will write clean & readable
Java source to an OutputStream/Writer.
55. Approach
1. Define the @Bind annotation.
2. Extend AbstractProcessor to create our
annotation processor for @Bind.
3. Within our processor: scan for all fields with
@Bind, keeping track of their parent classes.
4. Generate SoupLadle.java with .bind methods
for each parent class containing bound fields.
74. Project/Module Config
• The annotation processor and binding
annotation class need to live in a “regular” java
module.
• Add the android-apt gradle plugin to your root
build.gradle.
• Add dependency records to your app’s
build.gradle.
75. Project/Module Config
• The annotation processor and binding
annotation class need to live in a “regular” java
module.
• Add the android-apt gradle plugin to your root
build.gradle.
• Add dependency records to your app’s
build.gradle.
80. Project/Module Config
• The annotation processor and binding
annotation class need to live in a “regular” java
module.
• Add the android-apt gradle plugin to your root
build.gradle.
• Add dependency records to your app’s
build.gradle.
83. Project/Module Config
• The annotation processor and binding
annotation class need to live in a “regular” java
module.
• Add the android-apt gradle plugin to your root
build.gradle.
• Add dependency records to your app’s
build.gradle.