Making Icepick: The Good, The Bad and The Ugly

Novoda has a reputation of building the most desirable apps for Android. We believe living and sharing a hack-and-tell culture is one way to maintain top-shelf quality.

Annotation processing. You have heard about it at some point. You have used either Dagger or AndroidAnnotations in your projects and now you know there’s some magic happening at compile time, but did you dare to dig down further?

If you did you may have started digging, but soon found yourself lost in these huge frameworks. So you may have moved to something smaller, like Butterknife, to get a better understanding on the mechanics. Perhaps you still are puzzled by the magic. Well, if any of the above is true and you’re curious to know how to write your own annotation processing library, then you’re on the same boat I was on a couple of weeks ago when I started writing Icepick. Do you want to help the Android community fight boilerplate code? Here’s what you need to know.

Writing an Annotation Processing library is a lot less complicated than you may think. First you need to declare your annotation:

Above states that Annotations can only be used on Fields and after having been processed, can be safely discarded during compilation. You can specify a path to your own Processor in the META-INF.services/javax.annotation.processing.Processor file inside your resources path. This will tell the javac compiler that it must invoke this service when it is compiling classes.

The easiest way to implement Processor is to subclass
AbstractProcessor, a superclass which provides most of the functionalities needed. You can use either @SupportedAnnotationTypes or getSupportedAnnotationTypes() to declare the annotations supported by your processor.

During the compilation phase, the processor is created and its process() method will be called. Don’t worry too much about the two parameters types, as you usually start the process method in this idiomatic way:

This reads like “For each annotation supported by this processor, get all the elements where this annotation has been used”. Since the IcicleProcessor is only responding to a single annotation we can get rid of the for loop and get all the elements with roundEnv.getElementsAnnotatedWith(Icicle.class).

Now we are in the java.lang.model.element territory, a wilderness of mysterious classes that deal with elements of the Java language. Let’s quickly smuggle in what we need and leave this creepy place as soon as possible.

If we want to generate code which saves and restores instance state we must collect the field name and type, along with the name of the containing class.

Before getting these values we must validate the element modifiers but we cannot manipulate the field through our helper classes if it is either private, static or final.

If we group the elements by their containing class, we can iterate through each class to generate an helper dual that eliminates the boilerplate code.

Everything required for compile time is now provided. But if you think that our work it’s over, you’re wrong. We can use the generated classes just like any other class, but I think it’d be a mistake to do so. We don’t want to wait for the compilation phase to generate classes that we can’t see at the moment. We want to hide them behind another API that will make the call transparent to the end user. If we use reflection we can get a reference of the helper method and execute it.

This way not only we avoid referencing the helper class directly, but we can hide the calls Bundles.saveInstanceState() and Bundles.restoreInstanceState() in a parent class so we only have to write it once.

Pretty neat, huh? Well, if life were that easy we would have seen more and more annotations helping developers writing less code. But as in movies, no hero is a true hero until he has to face a badass enemy.

Since its code is executed at compile time, a Processor is extremely hard to debug. I’m not saying it’s impossible, but it’s hard enough to discourage most developers. You want to get your code right so that you don’t need to debug it. And the only way to avoid debugging is testing.

Funnily enough, Processors are also pretty hard to test. In a certain way, they share most of the problems that we have in testing Android applications. You do not create a Processor directly, just like you do not create Activities. This prevents you from passing it additional collaborators or dependencies. You must rely on a ProcessingEnvironment that holds a reference on pretty much anything you need, much like Activities rely on Context to reach to every other objects. Both ProcessingEnvironment and Context are God Objects extremely hard to mock and thus even more dangerous.

These problems share a common solution. The class which is hard to test should do as few things as possible, while delegating most of the logic to other objects easier to test. Let’s look at the IcicleWriter for example. Is it the processor’s responsibility to create a helper class or can extract a more focused object which can deal with this problem? Since it’s mainly doing string manipulation this object will be extremely easy to test and at lest we can assume that the code generation part is correct.

Another candidate ready to be extract in a separate class is the logic that determine which is the correct method to invoke on the Bundle to save or restore our fields. My first implementation of the IcicleConverter made heavy use of the Elements and Types utilities that you can get from the ProcessingEnvironment to reason over classes, generic collections and inheritance. Testing and mocking classes inside the java.lang.model.element will not make your life easier, so I was extremely unsatisfied by the overall result. Then I realized that if I can work on the class names instead of the class types I can code 99% of the behavior just doing String manipulation, a testing paradise. For the last 1% of the behavior (that is, a class that implements the Pacelable or Serializable interface) I could hide Elements and Types in a very simple IcicleAssigner and mock it when needed.

Finally, even Bundles can be written test-first. It is just a matter of extracting an internal object that does not use static methods and keep it hidden from the external caller. This way, the caller only knows about Bundles, whose job is to enforce type check on the callers, while BundleInjector can be constructed and tested without any restriction. You can start your tests trying to call a simple class by name and slowly work your way until you can correctly call a method from the helper classes.

There is a special character in this story. Sometimes he may look like a foe, although his intentions are good. That’s the new Android Build System. Since I’m a big fan of Gradle I was eager to switch to the new Android Studio IDE. And I must say, I was extremely impressed by how easy it was to deploy to Maven Central using Gradle (and it could be even easier with the Gradle Nexus Plugin).

However, as soon as I tried out Icepick in the wild, a problem became apparent. The library compiles against Android API 14 because it makes use of Fragments to enforce type checking and this was causing conflicts on older version of Android. The build tools documentation explicitly states that if you’re using the Android API on your library you should use the Gradle android-library plugin. Switching from java to the android-library plugin means that you have to deploy your artifact as an AAR and that you don’t have access to a unit test sourceSet (you can only run instrumentation tests against a device).

Thankfully, creating a provided scope in Gradle is not that complicated. The Gradleware team is not particularly keen on supporting the provided scope, but this solution works well for the moment. I’m looking forward to see the Andorid tools getting better and better. Keep up the good work guys!

About Novoda

We plan, design, and develop the world’s most desirable Android products. Our team’s expertise helps brands like Sony, Motorola, Tesco, Channel4, BBC, and News Corp build fully customized Android devices or simply make their mobile experiences the best on the market. Since 2008, our full in-house teams work from London, Liverpool, Berlin, Barcelona, and NYC.

Let’s get in contact