Better Analytics in Android with Annotation Processing and KotlinPoet

Leveraging the power of code generation to simplify analytic events.

Malvin Sutanto
Wantedly Engineering
7 min readApr 17, 2019

--

Photo by Maresa Smith / DTS

Nowadays, it’s very common to require analytics for your Android application to track your app’s performance. With so many 3rd party services and libraries out there, you’re usually spoiled with choices.

Using these analytics is also very straightforward. They usually come with a very simple and easy to use APIs. Typically, you will pass a list of keys and values to a function that is provided by the analytics SDK.

For example in the Firebase logEvent API which uses Bundle:

Managing these event parameters can get cumbersome really quickly since the keys and values are arbitrary and can also change rapidly depending on your analytic goals. Moreover, you need to ensure that you’re passing the correct key-value pairs or you will receive incorrect analysis.

With this article, I want to show how we can leverage Kotlin’s sealed class together with annotation processing and code generation to solve these problems. Ideally, we will use the Kotlin class’ constructor parameters and generate the code that convert these parameters to our analytics events’ key-values arguments.

How our generated function will look like.

Setting up

Let’s create a new Android app in Android Studio and add a new module named tracker that acts as our analytics tracking tools. In this tracker module, we will create an EventTracker that accepts a Bundle as a parameter for events.

For annotation processing to work, we will need to create 2 separate modules. An annotation module that contains the annotation that we will use to annotate our analytics events. And processor module that contains the annotation processing and code generation.

We will start by adding the required dependencies to our processor module in build.gradle:

Note: Both annotation and processor modules need to be Java/ Kotlin libraries instead of Android libraries.

Let’s add our annotation to annotation module and name it AnalyticsEvent:

Now, let’s create our annotation processor in processor module AnalyticsEventProcessor which extends from KotlinAbstractProcessor and register it to the build flow by adding Google’s auto-service annotation:

There are other options that you can add to your annotation processor, you can look for the list of options here.

Dependency graph for our modules.

Processing your annotated class

To start processing classes which are annotated with @AnalyticsEvent annotation, we will call roundEnv.getElementsAnnotatedWith() that returns a list of all elements which has been annotated with our annotation. And then check whether each element in the list is a Kotlin sealed class or not, using extension property .kotlinMetadata from the KotlinMetadata library.

Next, we will iterate through all the child classes using enclosedElements and check whether each child class extends from the enclosing sealed class using typeUtils.

We can then get the class’ name and the constructor’s parameters to generate our codes. Here, we can make use of the KotlinPoet and KotlinMetadata library to help us.

Our processor will now look like this:

Generating codes with KotlinPoet

Since we now have all the required data which we need to generate our codes, we are ready to write the code generation part of our processor.

To create an extension function, we can use .receiver(class) method from FunSpec.Builder. But, as we don’t have the dependency to the AnalyticsTracker nor Bundle, we need to do some workaround by utilizing the ClassName class.

You can take a look at KotlinPoet’s documentation here.

We will then add our when statement that converts our event to Bundle. To make this easier, we will make use of bundleOf function from androidx library.

Edit: KotlinPoet’s 1.1.0 Added support for MemberName for top level functions and members. Use that instead of ClassName.

We will use guava library to convert the class and parameter name from UpperCamelCase and lowerCamelCase to snake_case. The guava library comes from AutoService dependency.

Now that we have our extensionFunSpecBuilder completed. We will use FileSpec to write our generated function to a .kt file.

Our generateCode function for AnalyticsEventProcessor will now look like this:

Using the generated code

To make use of our newly generated analytics code, add the required dependencies to our app module’s build.gradle:

Then run Build -> Rebuild Project and we should be able to see our generated class under the generated folder.

Note: There’s a bug in Android Studio 3.3.0 where AS does not recognize files generated into build/generated/source/kaptKotlin, this has been fixed in AS 3.3.1 https://issuetracker.google.com/issues/122883561.

Once the classes have been successfully generated. We can invoke the generated function anywhere within our application like such:

Other benefit of using this approach is that we have code completion to help us easily call our analytics events. Also, since we’re using Kotlin, we can utilize named parameters to improve the readability of our events.

Now run and deploy our APK to the device or emulator to confirm that the analytics event is logged to Logcat when you press the button.

Summary

Annotation processing and code generation have a wide range of application and can be used for repeatable mundane codes that can easily be automated so that developers can focus on building features that gives values to the users.

There might be some initial investment that we have to incur in order to build the annotation processing, but I believe that the benefits and productivity improvements will outweigh the cost.

I hope this article can help you understand how annotation processing and code generation works and allow you to build your own annotation processing for your Android projects.

You can find the sample Github project here:

--

--