Android Annotation Processing Tutorial: Part 3: Generate Java Source Code

Android Annotation Processing Tutorial: Part 3: Generate Java Source Code
cover-android-annotation-processing-tutorial-part-3

We must make our strategy before we jump into implementation. This will reduce the number of hit and trials.

The source code for this tutorial can be found here: https://github.com/MindorksOpenSource/annotation-processing-example

Link to other parts of the tutorial:

  1. Part1 : A practical approach
  2. Part2: The project structure
  3. Part4:Use the generated code

Things that annotation processing provides while processing the Java annotated source code:

  1. Set<? extends TypeElement>: It provides a list of annotations as elements that are contained in the Java file being processed.
  2. RoundEnvironment: It provides access to the processing environment with utils to querying elements. Two main functions we will use from this environment are: processingOver(A mean to know if its the last round of processing) and getRootElements(It provides a list of elements that will get processed. Some of these elements will contain the annotation that we are interested.)

So, we have a set of annotations and a list of elements. Our library will generate a wrapper class that will help to map the views and clicks listeners for an activity.

It will have the following usage:

activity_main.xml defines a TextView with id tv_content and two buttons with id bt_1 and bt_2 . Our annotations will map the view and buttons to remove the boilerplate just like ButterKnife.

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_content)
    TextView tvContent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Binding.bind(this);
    }

    @OnClick(R.id.bt_1)
    void bt1Click(View v) {
        tvContent.setText("Button 1 Clicked");
    }

    @OnClick(R.id.bt_2)
    void bt2Click(View v) {
        tvContent.setText("Button 2 Clicked");
    }
}

We will use the MainActivity definition to auto generate our wrapper class named MainActivity$Binding using annotation processing.

Note: Any Activity in which we will use our annotations will result in the creation of a wrapper class ending with $Binding in its name. Example: If we have another activity say ProfileActivity that has @BindView or @OnClick usage then it will result in the creation of the ProfileActivity$Binding Java source code file.

After processing following class will be created.

@Keep
public class MainActivity$Binding {
  public MainActivity$Binding(MainActivity activity) {
    bindViews(activity);
    bindOnClicks(activity);
  }

  private void bindViews(MainActivity activity) {
    activity.tvContent = (TextView)activity.findViewById(2131165322);
  }

  private void bindOnClicks(final MainActivity activity) {
    activity.findViewById(2131165218).setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        activity.bt1Click(view);
      }
    });
    activity.findViewById(2131165219).setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        activity.bt2Click(view);
      }
    });
  }
}

Now that we know what we have to generate, let's analyse how to create it using the information that we have at our disposal while processing.

  1. We will first filter out those classes (Type) Elements that either use @BindView or @OnClick from the list of elements provided by the getRootElements method.
  2. We will then iterate over those filtered Elements and then scan through their members and methods to develop the class schema of the wrapper class using JavaPoet. In the end, we will write that class into a Java file.

For step 1 we would want to make the search efficient. So, we will create a class ProcessingUtils that will have the filtering method.

public class ProcessingUtils {

    private ProcessingUtils() {
        // not to be instantiated in public
    }

    public static Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
                                                            Set<? extends Element> supportedAnnotations) {
        Set<TypeElement> typeElements = new HashSet<>();
        for (Element element : elements) {
            if (element instanceof TypeElement) {
                boolean found = false;
                for (Element subElement : element.getEnclosedElements()) {
                    for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
                        for (Element annotation : supportedAnnotations) {
                            if (mirror.getAnnotationType().asElement().equals(annotation)) {
                                typeElements.add((TypeElement) element);
                                found = true;
                                break;
                            }
                        }
                        if (found) break;
                    }
                    if (found) break;
                }
            }
        }
        return typeElements;
    }
}

Here two things we need to understand:

  1. element.getEnclosedElements(): Enclosed elements are the elements that are contained in the given element. In our case the element will be MainActivity (TypeElement) and the Enclosed elemetents will be tvContent, onCreate, bt1Click, bt2Click and other inherited members.
  2. subElement.getAnnotationMirrors(): It will provide all the annotations used on the subElement. Example: @Override for onCreate, @BindView for tvContent and @OnClick for the bt1Click.

So, getTypeElementsToProcess will filter MainActivity as the TypeElement that we need for processing.

Now, we will scan all the filtered elements to create the corresponding wrapper class.

Important points:

  1. Find package of the element: elementUtils.getPackageOf(typeElement).getQualifiedName().toString()(in our case: com.mindorks.annotation.processing.example)
  2. Get simple name of the element: typeElement.getSimpleName().toString() (in our case MainActivity)
  3. We need ClassName to work with the annotation APIs: ClassName.get(packageName, typeName) (It will create a ClassName for MainActivity)
  4. We will have to create a ClassName for the Wrapper Class MainActivity$Binding so that we can define its members and methods.
Note: To facilitate name maintenance and a good coding practice we will create a class named NameStore. It will hold all the class, variables and method names that we need while defining the Binding class.
public final class NameStore {

    private NameStore() {
        // not to be instantiated in public
    }

    public static String getGeneratedClassName(String clsName) {
        return clsName + BindingSuffix.GENERATED_CLASS_SUFFIX;
    }

    public static class Package {
        public static final String ANDROID_VIEW = "android.view";
    }

    public static class Class {
        // Android
        public static final String ANDROID_VIEW = "View";
        public static final String ANDROID_VIEW_ON_CLICK_LISTENER = "OnClickListener";
    }

    public static class Method {
        // Android
        public static final String ANDROID_VIEW_ON_CLICK = "onClick";

        // Binder
        public static final String BIND_VIEWS = "bindViews";
        public static final String BIND_ON_CLICKS = "bindOnClicks";
        public static final String BIND = "bind";
    }

    public static class Variable {
        public static final String ANDROID_ACTIVITY = "activity";
        public static final String ANDROID_VIEW = "view";
    }
}

Also, you will find that the $Binding suffix is added in the binder-annotations library's (internal -> BindingSuffix) class. This is done for two purposes.

  1. We want the name to be configurable, i.e. we can change it from $Binding to _Binder or anything else.
  2. It will be used for finding the generated class in both binder and binder-compiler library.

JavaPoet Crash Course:

JavaPoet makes it really simple to define a class structure and write it while processing. It creates classes that are very close to a handwritten code. It provides facilities to auto infer the imports as well as beautify the code.

To use JavaPoet we need to add the following dependency into binder-compiler module.

dependencies {
    implementation project(':binder-annotations')
    implementation 'com.squareup:javapoet:1.11.1'
}

Note: Using JavaFileObject is very impractical and cumbersome. So, we will not even talk about it.

Basic usage of the JavaPoet that is required for this tutorial (Any advance understanding can be obtained from its GitHub Repo.)

  1. TypeSpec.Builder: Defines the class schema.
  2. addModifiers (Modifier): Add private, public or protected keyword.
  3. addAnnotation: Add an annotation to the element. Example: @Override on methods or @Keep on class in our case.
  4. TypeSpec.Builder -> addMethod: Add methods to the class. Example: constructor or other methods.
  5. MethodSpec -> addParameter: Add parameter type and its name for the method. Example: In our case, we want to pass MainActivity type with variable name activity to the method.
  6. MethodSpec -> addStatement: It will add the code blocks inside a method. In this method we 1st define the placeholders of the statement and then pass the paramenters to map those placeholders. Example: addStatement("$N($N)", "bindViews", "activity") (this will generate code bindViews(activity)). PlaceHolders: $N -> names, $T -> type(ClassName), $L -> literals(long etc.).
Rest of the things can be easily understood with reference to this basic introduction to the JavaPoet. I leave rest up to you to figure out. This is how I learned.

Final step: write the java source code.

It is really simple to write the defined class schema with the JavaPoet.

// write the defines class to a java file
try {
    JavaFile.builder(packageName,
            classBuilder.build())
            .build()
            .writeTo(filer);
} catch (IOException e) {
    messager.printMessage(Diagnostic.Kind.ERROR, e.toString(), typeElement);
}

It will generate the souce code in the folder. /app/build/generated/source/apt/debug

In our case: /app/build/generated/source/apt/debug/com/mindorks/annotation/processing/example/MainActivity$Binding.java

We will see the use of this generated class in the final part of this tutorial: Part 4

Thanks for reading this article. Be sure to share this article if you found it helpful. It would let others get this article and spread the knowledge.

Let’s become friends on Twitter, Linkedin, Github, and Facebook.

Learning is a journey, let’s learn together!