Write code that writes code! A beginner's guide to Annotation Processing - Jason Feinstein

Preview:

Citation preview

Write code that writes code!A beginner’s guide to annotation processing.

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.

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?

–http://docs.oracle.com/javase/1.5.0/docs/guide/language/annotations.html

“…an annotation is a form of syntactic metadata…”

Annotations

–http://docs.oracle.com/javase/1.5.0/docs/guide/language/annotations.html

“Annotations do not directly affect program semantics, but they do affect the way programs are treated by tools and libraries, which can in

turn affect the semantics of the running program.”

Annotations

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.

Custom annotations are useless..

… until you use them.

Custom Annotations• Useless

• Useless (until you actually use them…)

Custom Annotations• Useless

• Useless

• Run-time - with reflection

• Compile-time “Annotation Processor”

• Useless (until you actually use them…)

Custom Annotations• Useless

–Everyone

“Reflection is slow and you should never use it.”

–Smart People

“Reflection is slow and you should try to avoid using it on the main thread.”

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

Annotation Processing

List unprocessed source files with

annotations.

Register Annotation Processors

Any Processors for them?

Run ProcessorsCompileNo* Yes

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 ProcessorsCompileNo* Yes

Why would you want to make one?

• Boilerplate Reduction

• Reducing Boilerplate

• Reduced Boilerplate

• ….

• It’s pretty cool.

Let’s make one.

“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.

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.

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.

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.

Define @Bind

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

Define @Bind

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

Define @Bind

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

Define @Bind

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

Define @Bind

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

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.

Extending AbstractProcessorpublic 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 } }

Extending AbstractProcessorpublic 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 } }

Extending AbstractProcessorpublic 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 } }

Extending AbstractProcessorpublic 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 } }

Extending AbstractProcessorpublic 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 } }

Extending AbstractProcessorpublic 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 } }

Extending AbstractProcessorpublic 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 } }

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.

Processing…

@Overridepublic 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 .. }

Processing…

@Overridepublic 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 .. }

Processing…

@Overridepublic 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 .. }

Processing…

@Overridepublic 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 .. }

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.

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); } catch (IOException e) { throw new RuntimeException(e); }

}

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); } catch (IOException e) { throw new RuntimeException(e); }

}

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); } catch (IOException e) { throw new RuntimeException(e); }

}

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); } catch (IOException e) { throw new RuntimeException(e); }

}

😱

There has to be a better way!?

There is a better way.

Introducing: JavaPoet

Introducing: JavaPoetBy Square (Of Course)

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.

JavaPoet - Hello WorldTypeSpec.Builder helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC) .addMethod(MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("System.out.println($S + args[0])", "Hello: ") .build());JavaFile.builder("jwf.soupladle", helloWorld.build()).build().writeTo(System.out);

package jwf.soupladle;import java.lang.String;public class HelloWorld { public static void main(String[] args) { System.out.println("Hello: " + args[0]); } }

JavaPoet - Hello WorldTypeSpec.Builder helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC) .addMethod(MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("System.out.println($S + args[0])", "Hello: ") .build());JavaFile.builder("jwf.soupladle", helloWorld.build()).build().writeTo(System.out);

package jwf.soupladle;import java.lang.String;public class HelloWorld { public static void main(String[] args) { System.out.println("Hello: " + args[0]); }}

JavaPoet - Hello WorldTypeSpec.Builder helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC) .addMethod(MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("System.out.println($S + args[0])", "Hello: ") .build());JavaFile.builder("jwf.soupladle", helloWorld.build()).build().writeTo(System.out);

package jwf.soupladle;import java.lang.String;public class HelloWorld { public static void main(String[] args) { System.out.println("Hello: " + args[0]); } }

JavaPoet - Hello WorldTypeSpec.Builder helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC) .addMethod(MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("System.out.println($S + args[0])", "Hello: ") .build());JavaFile.builder("jwf.soupladle", helloWorld.build()).build().writeTo(System.out);

package jwf.soupladle;import java.lang.String;public class HelloWorld { public static void main(String[] args) { System.out.println("Hello: " + args[0]); } }

JavaPoet - Hello World

👏

TypeSpec.Builder helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC) .addMethod(MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("System.out.println($S + args[0])", "Hello: ") .build());JavaFile.builder("jwf.soupladle", helloWorld.build()).build().writeTo(System.out);

package jwf.soupladle;import java.lang.String;public class HelloWorld { public static void main(String[] args) { System.out.println("Hello: " + args[0]); } }

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.

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

"target.$L = ($T) target.findViewById($L)"

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

"target.$L = ($T) target.findViewById($L)"

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

"target.$L = ($T) target.findViewById($L)"

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

"target.$L = ($T) target.findViewById($L)"

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

👀

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // .. process annotations ..

try { JavaFileObject jfo = mFiler.createSourceFile("jwf.soupladle.SoupLadle"); TypeSpec.Builder soupLadleBuilder = TypeSpec.classBuilder("SoupLadle") .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (Map.Entry<TypeElement, List<VariableElement>> binding : bindingClasses.entrySet()) { TypeName typeParameter = ClassName.get(binding.getKey()); MethodSpec.Builder bindingBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .addParameter(typeParameter, "target"); List<VariableElement> members = binding.getValue(); for (VariableElement member : members) { Bind annotation = member.getAnnotation(Bind.class); TypeName castClass = ClassName.get(member.asType()); bindingBuilder.addStatement("target.$L = ($T) target.findViewById($L)", member.getSimpleName().toString(), castClass, annotation.value()); } soupLadleBuilder.addMethod(bindingBuilder.build()); } AnnotationSpec suppressIdentifierWarningAnnotation = AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType").build(); soupLadleBuilder.addAnnotation(suppressIdentifierWarningAnnotation); JavaFile file = JavaFile.builder("jwf.soupladle", soupLadleBuilder.build()).build(); Writer out = jfo.openWriter(); file.writeTo(out); out.flush(); out.close(); } catch (IOException e) { throw new RuntimeException(e); } return true; }

We’ve got an annotation processor now!

How do we tell the build process about it?

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.

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.

SoupLadle Module

SoupLadle Module

apply plugin: 'java'dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.squareup:javapoet:1.7.0'}

SoupLadle Module

jwf.soupladle.AnnotationProcessor

SoupLadle Module

include ':app', ':library'

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.

Project build.gradlebuildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.1.3' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' } } allprojects { repositories { jcenter() }} task clean(type: Delete) { delete rootProject.buildDir}

Project build.gradlebuildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.1.3' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' } } allprojects { repositories { jcenter() }} task clean(type: Delete) { delete rootProject.buildDir}

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.

App build.gradleapply plugin: 'com.android.application'apply plugin: 'com.neenbedankt.android-apt'android { compileSdkVersion 24 buildToolsVersion "24.0.2" defaultConfig { applicationId "jwf.soupladle.example" minSdkVersion 16 targetSdkVersion 24 versionCode 1 versionName "1.0" } buildTypes { // .. your build types .. } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.1.1' apt project(':library') provided project(':library') }

App build.gradleapply plugin: 'com.android.application'apply plugin: 'com.neenbedankt.android-apt'android { compileSdkVersion 24 buildToolsVersion "24.0.2" defaultConfig { applicationId "jwf.soupladle.example" minSdkVersion 16 targetSdkVersion 24 versionCode 1 versionName "1.0" } buildTypes { // .. your build types .. }} dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.1.1' apt project(':library') provided project(':library') }

App build.gradleapply plugin: 'com.android.application'apply plugin: 'com.neenbedankt.android-apt'android { compileSdkVersion 24 buildToolsVersion "24.0.2" defaultConfig { applicationId "jwf.soupladle.example" minSdkVersion 16 targetSdkVersion 24 versionCode 1 versionName "1.0" } buildTypes { // .. your build types .. }} dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.1.1' apt project(':library') provided project(':library') }

Let’s try using it!

activity_main.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="jwf.soupladle.example.MainActivity"> <TextView android:id="@+id/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!"/></RelativeLayout>

MainActivity.java

package jwf.soupladle.example;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import jwf.soupladle.Bind; public class MainActivity extends AppCompatActivity { @Bind(R.id.hello_world) public TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}

MainActivity.java

package jwf.soupladle.example;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import jwf.soupladle.Bind; public class MainActivity extends AppCompatActivity { @Bind(R.id.hello_world) public TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}

rebuild project…

A wild SoupLadle.java Appears!

package jwf.soupladle;import android.widget.TextView;import java.lang.SuppressWarnings; import jwf.soupladle.example.MainActivity;@SuppressWarnings("ResourceType") public final class SoupLadle { public static final void bind(MainActivity target) { target.textView = (TextView) target.findViewById(2131427412); }}

MainActivity.java

package jwf.soupladle.example;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import jwf.soupladle.Bind;import jwf.soupladle.SoupLadle;public class MainActivity extends AppCompatActivity { @Bind(R.id.hello_world) public TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); SoupLadle.bind(this); textView.setText("The binding worked!"); }}

Thank you! Questions?Twitter: @jasonwyatt

github.com/jasonwyatt bandcamp.com/jasonwyatt

Source Code available at: github.com/jasonwyatt/Soup-Ladle

Recommended