Android Custom Annotation

What is Annotation?

From Wikipedia:

In the Java computer programming language, an annotation is a form of syntactic metadata that can be added to Java source code. Classes, methods, variables, parameters and Java packages may be annotated.

As a Java or Android Developer, we are all familiar with annotation, using them gain you (and your codebase) a lot of benefits, from making the code cleaner (eg autogerated some stuffs) to safer (eg some checking constrain or pre-condition).

With Android developer, most of us started to know about the Annotation from the very basic annotation such as @Override, @Nullable or @NonNull… They are the annotations come with the Java and Android standard (or from the Google dependency android.support.annotation). Mainly, they are used as the pre-conditions for Lint or Java complier. That is another story, this post will try to cover 2 significan cases that we usually use Annotation to generate the source code in Android development (and also in Java’s world).

Using Annotation in Running Time

There is one famous Android library using this approach to make things done, Retrofit. Lets briefly list down how we are using Retrofit everyday

  • Retrofit prodives a list of custom annotations such as @Get @Post @Header
  • We are some interfaces which provided annotations depend on our purpose of HTTP communication
  • By using the RetrofitAdapter, we creat instances of our defined interface
  • We feel our code base is so clean, and implementing HTTP communication is so easy like making an instand noodles.

Basically, this approach is using the Proxy pattern, combined with the Reflection to get all the annotation definitions, and then create the handlers inside the proxy class to process the data. Lets learn with the real example.

My app need to handle the view and click tracking event of every element we show on the screens. The event will contains multiple infomation such as pageId, pageName, targetType and targetData. Forget about the real meaning for those param, but the pageId pageName and targetType are always static (for a specific event), and all events on same screen must have a same pageId (same session).

Yes, there are many ways for us to archive this, but I decide to follow the Retrofit method, where we define each track session as an interface, and each interface will contain the click/view method for each event.

Here is the annotation definations:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackingSession {
    @NonNull String pageType();

    @NonNull String pageSection() default "";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Click {
    @NonNull String pageType() default "";

    @NonNull String pageSection() default "";

    @NonNull String targetType() default "";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Impression {
    @NonNull String pageType() default "";

    @NonNull String pageSection() default "";

    @NonNull String targetType() default "";
}

Then I have an util object, which is a singleton to responsible for creating the proxy in the background

object TrackingSessionUtil {

    fun <T : Any> create(aClass : Class<T>) : T {
        validate(aClass)

        return TrackingSessionProxy(System.nanoTime().toString(), session).instance
    }

    fun <T> validate(aClass : Class<T>) {
        if (!aClass.isInterface) {
            throw IllegalArgumentException("Tracking Session declarations must be interfaces.")
        }

        if (aClass.interfaces.isNotEmpty()) {
            throw IllegalArgumentException("API interfaces must not extend other interfaces.")
        }
    }
}

And the proxy wrapper class:

internal class TrackingSessionProxy<T>(val pageId: String, aClass : Class<T>) {
    private val methodCache = HashMap<Method, SoftReference<TrackingSessionCall>>()
    val instance : T
    init {
        instance = Proxy.newProxyInstance(session.classLoader, arrayOf(aClass)) { proxy, method, args ->
            var call : TrackingSessionCall? = methodCache[method]?.get()
            if (call == null) {
                call = parse(aClass, method, args)
            }
            call.apply { send(this) }
            true
        } as T
    }
}

As you can see, we pass the interface (as T) to the util, then we use Proxy.newProxyInstance to create an instance for that interface. The lambda is the instance of InvocationHandler, which will be triggered everytime your interface proxy instance calling its pre-defined methods. Few things to list down

  • aClass : Class<T>: this is where you extract the annotation data with targeted to Class
  • method : Method: this is where you extract the annotation data with targeted to Method
  • args: Object[]: this is where you extract the annotation data with targeted to Method’s input

Pros

  • Very easy to implement
  • Code base is elegant
  • No source code generated, less method count (if it’s a matter for you)

Cons

  • Using reflection can be a small risk for performance, espeacially on the low devices
  • The logic is hidden behind the proxy, can be hard to understand and maintain

Using Annotation to generate source code

Another way to use the annotation that I want to discuss is, generating source code based on them.

There are many libraries using this approach, we can tell such as Butterknife, AndroidAnnotation, Dagger… Basically, we will read the annotation definition, then we will create an injector to inject into the complier which will handle the source code generating. There is many tutorial to do this, you can search on the internal. But here is the briefly steps to do (on Android):

  1. Create a Java Library
  2. Create a class CustomProcessor (or any name), which extended from AbstractProcessor
  3. Create a file in src/main/java/resource, name it as javax.annotation.processing.Processor, and put the path of your custom processor into it. This is the way we register our processor to the compiler.
  4. In the CustomProcessor, you will need to override 4.1 getSupportedAnnotationTypes() : tell the compiler which Annotation Type this processor will handle
   @Override
    public Set<String> getSupportedAnnotationTypes() {
        return new HashSet<>(Arrays.asList(TrackingSession.class.getCanonicalName()));
    }

4.2 getSupportedSourceVersion: tell which source version this processor supported

   @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

4.3 The most important, process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv): this is where you will create your magic, read the annotation metadata, generate the source code (with some library such as JavaPoet). The Processor provide an instance called processingEnv, this is where you can get the access to the Filer (to write a new file), or the Messenger (to write some log) and many more. The RoundEnvironment roundEnv is the place you can get the annotation metadata.

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

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(TrackingSession.class);

        for (Element element : elements) {
            if (element instanceof TypeElement) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "path " + element.asType().toString());
                SessionTrackingElement sessionTrackingElement = new SessionTrackingElement((TypeElement) element);
                generateSource(sessionTrackingElement);
            }
        }
        return true;
    }

Pros

  • No Reflection, no runtime usage, clearly this approach will guarantee your app performances
  • Because the logic will be written into the generate source code, so you can easily to trace down, put break point and easy to understand the magic behind

Cons

  • Require more effort, especially the generating code phase (you will need to understand the JavaPoet synctax, have all the logic that need to be done in your mind)
  • Within the big application, it can dramatically slow down the app compiling time. Also it will increase the method count and classes (obviously)

Conclusion

Both methods have its prons and cons, but the benefits it bring somehow will overcome all the disavantage. So we, developer, our job is how to use it wisely and helpful.