@AnnotationProcessors("ByExample") (Droidcon NYC 2015)

@AnnotationProcessors("ByExample") (Droidcon NYC 2015)

Annotations and Annotation Processors are all the rage in Android Development these days. There are annotations to help you:

- Avoid the boilerplate of Parcelable or ContentProviders
- Generate bindings for your views
- Implementing deeplinking
- Even find bugs in your code!

This talk will showcase some existing annotations (like the support-annotations library) and annotation processors (such as butterknife, deeplinkdispatch, autoparcel). Afterwards, I’ll show you how to write your own.

48fd642048ccd225ddaffcada7a6d407?s=128

Michael Evans

August 28, 2015
Tweet

Transcript

  1. @AnnotationProcessors("ByExample") Michael Evans

  2. LET ME EXPLAIN YOU ANNOTATIONS

  3. public class MainActivity extends AppCompatActivity {
 
 @Override protected void

    onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 }
 
 @Override public boolean onCreateOptionsMenu(Menu menu) {
 getMenuInflater().inflate(R.menu.menu_main, menu);
 return true;
 }
 
 }
  4. public class MainActivity extends AppCompatActivity {
 
 @Override protected void

    onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 }
 
 @Override public boolean onCreateOptionsMenu(Menu menu) {
 getMenuInflater().inflate(R.menu.menu_main, menu);
 return true;
 }
 
 }
  5. • Java 1.5 - JSR 175 • “A metadata facility

    that would allow classes, interfaces, fields, and methods to be marked as having particular attributes.” • @Override, @Deprecared, @SuppressWarnings (and friends) Annotations
  6. • @Override • Causes a compile error if the method

    is not found in one of the parent classes or implemented interfaces. • @Deprecated • Causes a compile warning if the method is used. • @SuppressWarnings • Instructs the compiler to suppress the compile time warnings
  7. Support Annotations

  8. • Nullness • @Nullable / @NonNull Support Annotations

  9. • Nullness • @Nullable / @NonNull • Resource Type annotations

    • @StringRes, @LayoutRes, @IdRes, etc Support Annotations
  10. • Nullness • @Nullable / @NonNull • Resource Type annotations

    • @StringRes, @LayoutRes, @IdRes, etc • @RequiresPermission Support Annotations
  11. • Nullness • @Nullable / @NonNull • Resource Type annotations

    • @StringRes, @LayoutRes, @IdRes, etc • @RequiresPermission • @Keep Support Annotations
  12. Support Annotations dependencies {
 compile 'com.android.support:support-annotations:22.2.0'
 } Go to Sebastiano

    Poggi’s “Tools of the Trade” talk
  13. Annotation Processors

  14. Annotations Processors • Java 1.6 - JSR 269 • Integrated

    into javac
  15. Why?

  16. Butterknife https://github.com/JakeWharton/butterknife

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


    EditText title = (EditText) findViewById(R.id.title);
 EditText subtitle = (EditText) findViewById(R.id.subtitle);
 Button submit = (Button) findViewById(R.id.submit);
 submit.setOnClickListener(new View.OnClickListener() {
 @Override public void onClick(View v) {
 //...
 }
 });
 } https://github.com/JakeWharton/butterknife
  18. Butterknife @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 


    EditText title = (EditText) findViewById(R.id.title);
 EditText subtitle = (EditText) findViewById(R.id.subtitle);
 Button submit = (Button) findViewById(R.id.submit);
 submit.setOnClickListener(new View.OnClickListener() {
 @Override public void onClick(View v) {
 //...
 }
 });
 } https://github.com/JakeWharton/butterknife
  19. Butterknife @Bind(R.id.title) EditText title;
 @Bind(R.id.subtitle) EditText subtitle;
 
 @Override protected

    void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 ButterKnife.bind(this);
 }
 
 @OnClick(R.id.submit)
 public void submit() {
 //...
 } https://github.com/JakeWharton/butterknife
  20. Dagger https://github.com/square/dagger

  21. • Dependency injector for Android and Java Dagger https://github.com/square/dagger

  22. Dagger • Dependency injector for Android and Java public class

    Example {
 @Inject Foo foo;
 @Inject Bar bar;
 
 public void baz() {
 foo.doSomething();
 bar.doSomethingElse();
 }
 } https://github.com/square/dagger
  23. Dagger • Dependency injector for Android and Java • That’s

    another show https://github.com/square/dagger public class Example {
 @Inject Foo foo;
 @Inject Bar bar;
 
 public void baz() {
 foo.doSomething();
 bar.doSomethingElse();
 }
 }
  24. https://github.com/airbnb/DeepLinkDispatch DeeplinkDispatch

  25. DeeplinkDispatch public class MainActivity extends AppCompatActivity {
 
 @Override protected

    void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 
 } https://github.com/airbnb/DeepLinkDispatch
  26. @DeepLink("foo://example.com/deepLink/{id}")
 public class MainActivity extends AppCompatActivity {
 
 @Override protected

    void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 
 if (getIntent().getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
 Bundle parameters = getIntent().getExtras();
 
 String id = parameters.getString("id");
 
 // Do something with the id
 }
 } DeeplinkDispatch https://github.com/airbnb/DeepLinkDispatch
  27. GAlette https://github.com/uPhyca/GAlette

  28. GAlette @SendEvent(category = "HelloWorld", action = "sayHello", label="%1$s")
 String sayHello

    (String name) {
 return String.format("Hello, %s.", name);
 } https://github.com/uPhyca/GAlette
  29. Droidcon NYC 2014 Annotation Processing Boilerplate Destruction

  30. Example Time

  31. Example Time startActivityForResult() onActivityResult()

  32. Example Time startActivityForResult() onActivityResult() Aftermath

  33. @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data) {


    // retrieve the error code, if available
 int errorCode = -1;
 if (data != null) {
 errorCode = data.getIntExtra(WalletConstants.EXTRA_ERROR_CODE, -1);
 }
 switch (requestCode) {
 case REQUEST_CODE_MASKED_WALLET:
 switch (resultCode) {
 case Activity.RESULT_OK:
 MaskedWallet maskedWallet =
 data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET);
 launchConfirmationPage(maskedWallet);
 break;
 case Activity.RESULT_CANCELED:
 break;
 default:
 handleError(errorCode);
 break;
 }
 break;
  34. case REQUEST_CODE_RESOLVE_ERR:
 if (resultCode == Activity.RESULT_OK) {
 mGoogleApiClient.connect();
 } else

    {
 handleUnrecoverableGoogleWalletError(errorCode);
 }
 break;
 case REQUEST_CODE_RESOLVE_LOAD_FULL_WALLET:
 switch (resultCode) {
 case Activity.RESULT_OK:
 if (data.hasExtra(WalletConstants.EXTRA_FULL_WALLET)) {
 FullWallet fullWallet =
 data.getParcelableExtra(WalletConstants.EXTRA_FULL_WALLET);
 fetchTransactionStatus(fullWallet);
 }
 }
  35. case REQUEST_CODE_CHANGE_MASKED_WALLET:
 if (resultCode == Activity.RESULT_OK &&
 data.hasExtra(WalletConstants.EXTRA_MASKED_WALLET)) {
 mMaskedWallet

    = data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET);
 ((FullWalletConfirmationButtonFragment) getResultTargetFragment())
 .updateMaskedWallet(mMaskedWallet);
 }
 break;
 case WalletConstants.RESULT_ERROR:
 int errorCode = data.getIntExtra(WalletConstants.EXTRA_ERROR_CODE, 0);
 handleError(errorCode);
 break;
 default:
 super.onActivityResult(requestCode, resultCode, data);
 break;
  36. • Usually several modules • “Annotations” (jar) • “Compiler” (jar)

    • “API” (jar/aar) Project Structure
  37. Part One: The Annotation

  38. @Target(ElementType.METHOD)
 public @interface OnActivityResult {
 int value();
 }

  39. @Target(ElementType.METHOD)
 public @interface OnActivityResult {
 int value();
 }

  40. @Target(ElementType.METHOD)
 public @interface OnActivityResult {
 int value();
 }

  41. @Target(ElementType.METHOD)
 public @interface OnActivityResult {
 int value();
 } default -1;


  42. Part Two: The Processor

  43. • com/example/YourProcessor.java • META-INF/services/javax.annotation.processing.Processor • com.example.YourProcessor Registering your Processor

  44. https://github.com/google/auto/tree/master/service AutoService

  45. import javax.annotation.processing.Processor;
 
 @AutoService(Processor.class)
 public final class MyProcessor extends AbstractProcessor

    {
  46. public abstract class AbstractProcessor implements Processor {
 
 Set<String> getSupportedAnnotationTypes();


    
 SourceVersion getSupportedSourceVersion();
 
 synchronized void init(ProcessingEnvironment processingEnv);
 
 boolean process(Set<? extends TypeElement> annotations,
 RoundEnvironment roundEnv);
 
 }

  47. @Override
 public Set<String> getSupportedAnnotationTypes() {
 return Collections.singleton( OnActivityResult.class.getCanonicalName());
 }
 


    @Override
 public SourceVersion getSupportedSourceVersion() {
 return SourceVersion.latestSupported();
 }
  48. @Override
 public synchronized void init(ProcessingEnvironment env) {
 super.init(processingEnv);
 filer =

    env.getFiler();
 messager = env.getMessager();
 elementUtils = env.getElementUtils(); typeUtils = env.getTypeUtils();
 }
  49. @Override
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {

  50. @Override
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {


    
 for (Element element : env.getElementsAnnotatedWith(OnActivityResult.class)) {
 messager.printMessage(Diagnostic.Kind.WARNING, element.getSimpleName());
 } return false; }
  51. public class Example { //TypeElement
 
 private int foo; //VariableElement


    
 public Example() { //ExecuteableElement
 
 }
 
 public void setFoo(int i) {
 foo = i;
 }
 }
  52. @Override
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

    for (Element element : roundEnv.getElementsAnnotatedWith(OnActivityResult.class)) {
 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
 BindingClass bindingClass = getOrCreateAftermath(targetClassMap, enclosingElement, erasedTargetNames);
 bindingClass.createAndAddResultBinding(element);
 ...
 } for (BindingClass bindingClass : targetClassMap.values()) {
 try {
 bindingClass.writeToFiler(filer);
 } catch (IOException e) {
 messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
 }
 }
  53. "package com.example;",
 "",
 "import android.content.Intent;",
 "import java.lang.Override;",
 "import org.michaelevans.aftermath.Aftermath;",
 "",


    "public class MainActivity$$Aftermath<T extends com.example.MainActivity>"
 +" implements Aftermath.IOnActivityForResult<T> {",
 " @Override",
 " public void onActivityResult(final T target, final int requestCode,"
 +" final int resultCode, final Intent data) {",
 " if(requestCode == 1) {",
 " target.onContactPicked(resultCode, data);",
 " }",
 " }",
 "}"
  54. https://github.com/square/javapoet A Java API for generating .java source files. 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);
  55. final class BindingClass { private final Map<Integer, OnActivityResultBinding> activityResultBindings; void

    createAndAddResultBinding(Element element) {
 OnActivityResultBinding binding = new OnActivityResultBinding(element);
 activityResultBindings.put(binding.requestCode, binding);
 } void writeToFiler(Filer filer) throws IOException {
 ClassName targetClassName = ClassName.get(classPackage, targetClass);
 TypeSpec.Builder aftermath = TypeSpec.classBuilder(className)
 .addModifiers(Modifier.PUBLIC)
 .addTypeVariable(TypeVariableName.get("T", targetClassName))
 .addMethod(generateOnActivityResultMethod());
 
 ClassName callback = ClassName.get("org.michaelevans.aftermath", "IOnActivityForResult");
 aftermath.addSuperinterface(ParameterizedTypeName.get(callback,
 TypeVariableName.get("T")));
 
 JavaFile javaFile = JavaFile.builder(classPackage, aftermath.build()).build();
 javaFile.writeTo(filer);
 } ...
  56. private MethodSpec generateOnActivityResultMethod() {
 MethodSpec.Builder builder = MethodSpec.methodBuilder("onActivityResult")
 .addAnnotation(Override.class)
 .addModifiers(Modifier.PUBLIC)


    .returns(void.class)
 .addParameter(TypeVariableName.get("T"), "target", Modifier.FINAL)
 .addParameter(int.class, "requestCode", Modifier.FINAL)
 .addParameter(int.class, "resultCode", Modifier.FINAL)
 .addParameter(ClassName.get("android.content", "Intent"), "data", Modifier.FINAL);
 
 if (!activityResultBindings.isEmpty()) {
 for (OnActivityResultBinding binding : activityResultBindings.values()) {
 builder.beginControlFlow("if (requestCode == $L)", binding.requestCode);
 builder.addStatement("target.$L(resultCode, data)", binding.name);
 builder.endControlFlow();
 }
 }
 
 return builder.build();
 }
  57. Part Three: The API

  58. @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {


    Aftermath.onActivityResult(this, requestCode, resultCode, data);
 super.onActivityResult(requestCode, resultCode, data);
 }
  59. public static void onActivityResult(Object target, int requestCode, int resultCode, Intent

    data) {
 Class<?> targetClass = target.getClass();
 if (debug) {
 Log.d(TAG, "Looking up aftermath for " + targetClass.getName());
 }
 IOnActivityForResult<Object> aftermath = findActivityForResultForClass(targetClass);
 if (aftermath != NO_OP) {
 aftermath.onActivityResult(target, requestCode, resultCode, data);
 }
 }
  60. package org.michaelevans.aftermath.sample;
 
 import android.content.Intent;
 import java.lang.Override;
 import org.michaelevans.aftermath.Aftermath;
 


    public class MainActivity$$Aftermath<T extends MainActivity> implements Aftermath.IOnActivityForResult<T> {
 @Override
 public void onActivityResult(final T target, final int requestCode, final int resultCode, final Intent data) {
 if (requestCode == 1) {
 target.onContactPicked(resultCode, data);
 }
 if (requestCode == 2) {
 target.onOtherRequest(resultCode, data);
 }
 }
 }

  61. • Can only generate new files • Can’t manipulate already

    existing files • Use android-apt Tips
  62. https://github.com/google/compile-testing assert_().about(javaSource())
 .that(JavaFileObjects.forResource("HelloWorld.java"))
 .processedWith(new MyAnnotationProcessor())
 .compilesWithoutError()
 .and()
 .generatesSources( JavaFileObjects.forResource("GeneratedHelloWorld.java"));

  63. https://github.com/MichaelEvans/Aftermath

  64. LET ME EXPLAIN YOU APPLAUSE

  65. Questions? twitter.com/m_evans10 google.com/+MichaelEvans michaelevans.org https://github.com/MichaelEvans/Aftermath