Android Annotation Processing Tutorial: Part 4: Use The Generated Code

cover-android-annotation-processing-tutorial-part-4

In this final part of the tutorial we will see the usage of the generated code while annotation processing:

Link to other parts of the tutorial:

  1. Part1 : A practical approach
  2. Part2: The project structure
  3. Part3: Generate Java source code

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

We will add the following dependencies in the app's build.gradle for the annotation processing to run.

dependencies {
    ...
    //annotation processing
    implementation project(':binder')
    annotationProcessor project(':binder-compiler')
}

Now, when we build our app module then we will find MainActivity$Binding class generated in the directory /app/build/generated/source/apt/debug. This class takes MainActivity in the constructor and then maps and casts the TextView as well as assign the OnClickListener to the buttons.

Since we have generated MainActivity$Binding based on the MainActivity's name so, we don't know the exact name of the generated class prior to the generation (MainActivity can be renamed to something else later in the project lifecycle). So, we need to dynamically find the generated class that maps the given activity.

We will define this functionality in our binding library module.

public class Binding {

    public Binding() {
        // not to be instantiated in public
    }

    private static <T extends Activity> void instantiateBinder(T target, String suffix) {
        Class<?> targetClass = target.getClass();
        String className = targetClass.getName();
        try {
            Class<?> bindingClass = targetClass
                    .getClassLoader()
                    .loadClass(className + suffix);
            Constructor<?> classConstructor = bindingClass.getConstructor(targetClass);
            try {
                classConstructor.newInstance(target);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Unable to invoke " + classConstructor, e);
            } catch (InstantiationException e) {
                throw new RuntimeException("Unable to invoke " + classConstructor, e);
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                }
                if (cause instanceof Error) {
                    throw (Error) cause;
                }
                throw new RuntimeException("Unable to create instance.", cause);
            }
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Unable to find Class for " + className + suffix, e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find constructor for " + className + suffix, e);
        }
    }

    public static <T extends Activity> void bind(T activity) {
        instantiateBinder(activity, BindingSuffix.GENERATED_CLASS_SUFFIX);
    }
}

Here we have used reflection API to create the instance of the MainActivity$Binding. This is the only place we have used the Reflection API. If we had known the name of the class prior to this then we would have used that class directly.

This is a typical example where the class name is based on the class being processed. So, using reflection is not a big issue. This is how ButterKnife works. In many cases, reflection APIs will not be used at all.

Now, we can use our MainActivity with Binding class:

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 can now create an Activity and use @BindView and @OnClick and Binding.bind(this) will do the magic.

Last thing:

Remember we had created annotation @Keep and used it on the MainActivity$Binding class. Now, it is time to understand its importance.

We are using reflection to instantiate the MainActivity$Binding class. If we apply the proguard while generating the signed APK then the class name will be obfuscated. So, Reflection API will not be able to find MainActivity$Binding class. To overcome this issue we have to tell the proguard to not obfuscate the classes that have @Keep annotation.

Also, we would want to add the progurad rule in the library itself so that client desn't have to worry about it.

To do so we need to do two things:

First: Add progurad rule in the binder library module's proguard-rules.pro file:

-keep class com.mindorks.lib.annotations.Keep**
-keep @com.mindorks.lib.annotations.Keep public class *
-keepclassmembers @com.mindorks.lib.annotations.Keep class ** { *;}

Second: Let the gradle know that this progurad rule has to be added in the module that uses it. We will have to add consumerProguardFiles in the build.gradle of the binder library module.

android {

    defaultConfig {
        ...
        consumerProguardFiles 'proguard-rules.pro'
    }
}

That's It!!

Hope you must have enjoyed this tutorial as much as I enjoyed writing it.

If you have reached this far reading all the 4 parts then you have my respect and I would definitely like to connect with you.

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

If you want to dive deeper into Annotation Processing then you can go through my library PlaceHolderView. It has much advanced implementations with generics and code separation.