How to code
to code less
github.com/anton-novikau
Anton Novikau
Lead Android Developer @ Viber Media
Boilerplate Fighters!
Agenda
What is Annotation Processor?
How does it work?
Project structure
Create Annotation Processor
How to debug Processor’s code?
Demo time!
How to setup a goal?
Java Annotation Processors
Processing Rounds
TODO BUILD DONE
D
A C
B
E
F
Round 1
Processing Rounds
TODO BUILD DONE
Round 1
D
A C
B
E
F
Processing Rounds
TODO BUILD DONE
Round 1
D
A C
B
E
F
Processing Rounds
TODO BUILD DONE
Round 1
AA
FA
FB D
A C
B
E
F
Processing Rounds
TODO BUILD DONE
Round 1
AA
FA
FB
D
A C
B
E
F
Processing Rounds
TODO BUILD DONE
Round 2
AA
FA
FB
D
A C
B
E
F
Processing Rounds
TODO BUILD DONE
Round 2
AA
FA
FB
D
A C
B
E
F
Processing Rounds
TODO BUILD DONE
Round 2
AA
FA
FB
AAA
D
A C
B
E
F
Processing Rounds
TODO BUILD DONE
Round 2
AAA
D
A C
B
E
F
AA
FA
FB
Processing Rounds
TODO BUILD DONE
Round 3
AAA
D
A C
B
E
F
AA
FA
FB
Processing Rounds
TODO BUILD DONE
Round 3
AAA
D
A C
B
E
F
AA
FA
FB
public class Person {
private String firstName;
private String lastName;
private Date birthday;
}
@Pojo
@Pojo
public class Person {
private String firstName;
private String lastName;
private Date birthday;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public Date getBirthday() {
return birthday;
}
Where to get source data to generate code?
Before we start
Inheritance vs Composition
What should be a start point for our processor?
Inheritance
public class Contact implements Parcelable {
}
public static final Creator<Contact> CREATOR =
new Creator<Contact>() {
@Override
public Contact createFromParcel(Parcel source) {
return new Contact(source);
}
@Override
public Contact[] newArray(int size) {
return new Contact[size];
}
};
...
Inheritance
public class Contact implements Parcelable {
}
public class Contact implements Parcelable {
}
Inheritance
...
public Contact() {
}
Contact(Parcel source) {
...
}
...
public class Contact implements Parcelable {
}
Inheritance
...
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
...
}
class Smart_Contact extends Contact {
// creator
// constructor
// methods from Parcelable
}
@SmartParcelable
public abstract class Contact implements Parcelable {
}
Inheritance
class Smart_Contact extends Contact {
// creator
// constructor
// methods from Parcelable
}
@SmartParcelable
public abstract class Contact implements Parcelable {
public static Contact create() {
return new Smart_Contact();
}
}
Inheritance
Composition
@RuntimePermissions
public class MainActivity extends AppCompatActivity {
...
public void onMigrateImageClicked(View v) {
MainActivityPermissionsDispatcher
.migrateImageWithPermissionCheck(this);
}
@NeedsPermission(WRITE_EXTERNAL_STORAGE)
void migrateImage() {
...
}
}
Composition
@RuntimePermissions
public class MainActivity extends AppCompatActivity {
...
public void onMigrateImageClicked(View v) {
MainActivityPermissionsDispatcher
.migrateImageWithPermissionCheck(this);
}
@NeedsPermission(WRITE_EXTERNAL_STORAGE)
void migrateImage() {
...
}
}
Composition
@RuntimePermissions
public class MainActivity extends AppCompatActivity {
...
public void onMigrateImageClicked(View v) {
MainActivityPermissionsDispatcher
.migrateImageWithPermissionCheck(this);
}
@NeedsPermission(WRITE_EXTERNAL_STORAGE)
void migrateImage() {
...
}
}
Where to get source data to generate code?
What should be a start point for our processor?
Before we start
Inheritance vs Composition
Effort of using your generated code vs creating a boilerplate code
Cursor boilerplate
public class ContactEntity {
long id;
String name;
Uri photoUri;
}
Cursor boilerplate
ContactEntity contact = new ContactEntity();
contact.setId(c.getLong(0));
contact.setName(c.getString(1));
contact.setPhotoUri(Uri.parse(c.getString(2)));
Cursor c = db.query("contacts",
"_ID = ?",
new String[] {"_ID", "NAME", "PHOTO"},
new String[] { String.valueOf(id) },
null, null, null);
ContactEntity contact = new ContactEntity();
contact.setId(c.getLong(0));
contact.setName(c.getString(1));
String uri = c.getString(2);
if (uri != null) {
contact.setPhotoUri(Uri.parse(uri));
}
Cursor boilerplate
Cursor c = db.query("contacts",
"_ID = ?",
new String[] {"_ID", "NAME", "PHOTO"},
new String[] { String.valueOf(id) },
null, null, null);
Cursor boilerplate
ContentValues values = new ContentValues();
values.put("_ID", contact.getId());
values.put("NAME", contact.getName());
values.put("PHOTO", contact.getPhotoUri().toString());
db.update("contacts",
values,
"_ID = ?",
new String[] { String.valueOf(contact.getId()) });
ContentValues values = new ContentValues();
values.put("_ID", contact.getId());
values.put("NAME", contact.getName());
String photoUri = contact.getPhotoUri() != null
? contact.getPhotoUri().toString()
: null
values.put("PHOTO", photoUri);
Cursor boilerplate
db.update("contacts",
values,
"_ID = ?",
new String[] { String.valueOf(contact.getId()) });
Goal
public class ContactEntity {
long id;
String name;
Uri photoUri;
}
Goal
@Entity(table = "contacts")
public class ContactEntity {
@Column(name = "_ID")
@PrimaryKey
long id;
@Column(name = "NAME")
String name;
@Column(name = "PHOTO", adapter = UriTypeAdapter.class)
Uri photoUri;
}
Project Structure
Project
api
processors
application
API Module
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
compileOnly 'com.google.android:android:4.1.1.4'
}
API Module
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
compileOnly 'com.google.android:android:4.1.1.4'
}
API Module
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Entity {
String table();
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Column {
String name();
Class<? extends TypeAdapter<?>> adapter();
}
API Module
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Entity {
String table();
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Column {
String name();
Class<? extends TypeAdapter<?>> adapter();
}
Processors Module
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation project(':api')
}
Processors Module
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation project(':api')
}
Create Annotation Processor
@SupportedAnnotationTypes("roomie.api.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EntityHelperProcessor extends AbstractProcessor {
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
...
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
...
}
}
Create Annotation Processor
@SupportedAnnotationTypes("roomie.api.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EntityHelperProcessor extends AbstractProcessor {
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
...
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
...
}
}
Create Annotation Processor
@SupportedAnnotationTypes("roomie.api.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EntityHelperProcessor extends AbstractProcessor {
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
...
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
...
}
}
Create Annotation Processor
@SupportedAnnotationTypes("roomie.api.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EntityHelperProcessor extends AbstractProcessor {
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
...
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
...
}
}
Register Processor
Project
processors
src
main
resources
META-INF
services
Old Fashioned way
javax.annotation.processing.Processor
roomie.codegen.EntityHelperProcessor
...
Register Processor
Modern way
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation project(':api')
implementation 'com.google.auto:auto-common:0.8'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
}
Register Processor
Modern way
@AutoService(Processor.class)
@SupportedAnnotationTypes("roomie.api.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EntityHelperProcessor extends AbstractProcessor {
...
}
Write a simple Class
package roomie.example;
public class DemoImpl extends Demo {
int counter;
public void print(String msg) {
System.out.println(msg);
}
}
Write a simple Class: raw text
JavaFileObject sourceFile =
filer.createSourceFile(qualifiedName, annotatedClass);
try (Writer writer = sourceFile.openWriter()) {
writer.append("package ");
writer.append(packageName).append(";nn");
writer.append("public class ").append(className);
writer.append(" extends ");
writer.append(annotatedClass.simpleName()).append(" {n");
writer.append(" int counter;nn");
writer.append(" public void print(String msg) {n");
writer.append(" System.out.println(msg);n");
writer.append(" }n");
writer.append("}n");
writer.flush();
}
Processors Module
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation project(':api')
implementation 'com.squareup:javapoet:1.9.0'
implementation 'com.google.auto:auto-common:0.8'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
}
JavaFile DemoImpl.java
TypeSpec class DemoImpl
MethodSpec void print()
FieldSpec int counter
ParameterSpec String msg
JavaPoet Java
=
Write a simple Class: JavaPoet
Define Class
TypeSpec.Builder classContent =
TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.superclass(annotatedClass);
Write a simple Class: JavaPoet
Define Method and Field
MethodSpec print =
MethodSpec.methodBuilder("print")
.addModifiers(Modifier.PUBLIC)
.addParameter(String.class, "msg")
.addStatement(“$L.out.println(msg)",
System.class)
.build();
FieldSpec counter =
FieldSpec.builder(int.class, "counter").build();
classContent.addMethod(print);
classContent.addField(counter);
Write a simple Class: JavaPoet
Write our Class to a File
JavaFile javaFile = JavaFile.builder(packageName,
classContent.build())
.indent(" ")
.build();
JavaFileObject sourceFile = filer.createSourceFile(
qualifiedName,
annotatedClass);
try (Writer writer = sourceFile.openWriter()) {
writer.write(javaFile.toString());
}
Don’t worry.
We’ve almost finished
Application Module
apply plugin: 'com.android.application'
android {
...
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
}
dependencies {
implementation project(':api')
annotationProcessor project(':codegen')
...
}
Application Module
apply plugin: 'com.android.application'
android {
...
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
}
dependencies {
implementation project(':api')
annotationProcessor project(':codegen')
...
}
Application Module
apply plugin: 'com.android.application'
android {
...
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
}
dependencies {
implementation project(':api')
annotationProcessor project(':codegen')
...
}
How to Debug?
How to Debug?
Setup Gradle Daemon
vim ~/.gradle/gradle.properties
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,
server=y,suspend=n,address=5005
Start Daemon and Connect
./gradlew --stop
./gradlew --daemon
./gradlew clean :sample:compileDebugJavaWithJavac
Demo Time
Conclusion
Thank you!
Questions?
Useful Links
Hannes Dorfmann - Annotation Processing 101
Eugenio Marletti - Pushing the limits of Kotlin annotation processing
Demo App (aka Roomie) Source Code

How to code to code less