Writing SOLID Analytics With Kotlin for Android

Fancy Diagram and Code Snippets Attached

This article will demonstrate how you could (should) decouple analytics libraries (or anything else) from your business logic code, effectively allowing you to add & remove analytics services and events on-the-fly.

Wakey’s Analytics opt-out dialog

Important Update ❤️

The project is now available on GitHub, and on a Maven Repository

Tightly Coupled Code

Baking analytics into Android apps has always been a monstrous process for me, but that’s only because I’ve been approaching it incorrectly. In my latest app, Wakey, I’ve implemented a few analytics events across the different screens and services using Fabric’s Answers SDK. The different SDK functions were scattered across the app’s different screens — which meant this:

If I wanted to remove or replace Fabric’s library, I would have to go through each and every Activity / Fragment / Service, and edit them one-by-one. The same goes for adding an additional analytics service — I would have to go over each event and duplicate it in some way.

I’ve had both CustomEvent calls and ContentView calls scattered across the app’s different screens, which looked something like this:

// Pre-Decoupling Code - inside Android Activity
// event trigger
Answers.getInstance().logCustom(CustomEvent("alarm_snoozed"))
// content view trigger
Answers.getInstance().logContentView(ContentViewEvent().putContentName("alarm_screen"))

Instead, I was hoping for a loosely-coupled implementation, which would look more like this:

// Desired Code
// event trigger
analytics.trackEvent(AlarmActionSnoozeEvent());
// content view trigger
analytics.trackContentView(AlarmActivityContentView())

We only want the Activity or Presenter to know about our Analytics utility. The presenter shouldn’t depend on Fabric, Flurry, Firebase — or any other fancy F-word analytics tool. It is aware of the events it needs to send, and the master analytics class. That’s it.


Dispatchers & Events Design

Let’s understand how the code we saw above is going to execute:

“Event — Analytics — Dispatcher” Flow Chat

The Analytics class will hold a list of dispatchers, sending each of them the event parameter passed from the different screens. Each dispatcher will implement its own way of handling the event, meaning we’ll have one dispatcher per analytics service. And so, the first thing we want, is a dispatcher interface that would handle events and contentViews:

interface AnalyticsDispatcher {

fun trackContentView(contentView: AnalyticsContentView)

fun trackCustomEvent(event: AnalyticsEvent)
}

The next step is to setup the interfaces for the ContentViews and Events. We know the events will hold a name, and maybe some parameters, while ContentViews will only hold a name only — for the purpose of this article.

interface AnalyticsEvent : BaseAnalyticsEvent {

fun getParameters(): Map<String, String> {
// default empty map implementation
return emptyMap()
}

fun getEventName(): String
}
interface AnalyticsContentView : BaseAnalyticsEvent {

fun getViewName(): String

}

Both interfaces above extend theBaseAnalyticsEvent interface — which we’ll cover later. For now, let’s implement our first event example:

class AlarmActionSnoozeEvent : AnalyticsEvent {
override fun getEventName() : String {
return "alarm_snoozed"
}

// could also override the getParameters() function...
}

We could now safely pass an instance of theAlarmActionSnoozeEvent into our analytics class, but it wouldn’t do anything just yet — as we have yet to implement any dispatchers.

For now, we’ll only have one dispatcher implementation:

class AnswersDispatcherImpl : AnalyticsDispatcher {

override fun trackCustomEvent(event: AnalyticsEvent) {
Answers.getInstance().logCustom(event.createAnswersEvent())
}

override fun trackContentView(contentView: AnalyticsContentView) {
Answers.getInstance()
.logContentView(contentView.createAnswersEvent())
}
}

the createAnswersEvent function is automatically included inside the Event and ContentView interfaces, as it is declared in the BaseAnalyticsEvent they both extend. Let’s create it now.

interface BaseAnalyticsEvent {
fun createAnswersEvent(): Any
}

Notice that we return an Any type, so we could later override the function with different types, which the Answers SDK requires. Let’s take a look:

import com.crashlytics.android.answers.CustomEvent
import com.crashlytics.android.answers.ContentViewEvent
interface AnalyticsEvent : BaseAnalyticsEvent {
    // previous functions from before...
    override fun createAnswersEvent(): CustomEvent {
return CustomEvent(this.getEventName()).apply {
getParameters().forEach {
putCustomAttribute(it.key, it.value)
} }
}
}
interface AnalyticsContentView : BaseAnalyticsEvent {
    // previous functions from before...

override fun createAnswersEvent(): ContentViewEvent {
return ContentViewEvent().putContentName(this.getViewName())
}
}

Analytics Class

Now, let’s build the class that is in charge of it all. The Analytics class constructor should have a parameter that will carry the list of Dispatchers. That’s the vararg keyword.

class Analytics(private vararg val dispatchers: AnalyticsDispatcher) {
fun trackEvent(contentView: AnalyticsEvent) {
// dispatch the event with every dispatcher
dispatchers.forEach { it.trackEvent(event) }
}
fun trackContentView(contentView: AnalyticsContentView) {
// dispatch the event with every dispatcher
dispatchers.forEach { it.trackContentView(event) }
}
}

Initiate it inside the Application class:

class App : Application() {
    private lateinit var analytics: Analytics
    override fun onCreate() {
super.onCreate()
        analytics = Analytics(AnswersDispatcherImpl())
    }
}

Almost Done

We can now call the analytics tool like we wanted to in the beginning of this article. The next step will show how easy it is to throw another analytics tool into the mix. In this example, we’ll use Flurry Analytics.

import com.flurry.android.FlurryAgent
class FlurryDispatcherImpl : AnalyticsDispatcher {

override fun trackCustomEvent(event: AnalyticsEvent) {
FlurryAgent
.logEvent(event.getEventName(), event.getParameters())
}

override fun trackContentView(contentView: AnalyticsContentView){
FlurryAgent
.logEvent("contentView_" + contentView.getViewName())
}

}

Back inside the Application’s onCreate function, we just add the Flurry dispatcher into the list of dispatchers.

// initiate the analytics with the different dispatchers
analytics = Analytics(AnswersDispatcherImpl(), FlurryDispatcherImpl())

That’s it. All that’s left is to implement the remaining events, and call the Analytics object from different places across the app.


Developed for Wakey

Wakey is a simple & beautiful animated alarm clock, featuring a spectacular design and an enjoyable experience — guaranteed to wake you up with a smile everyday!

With our smiling sunrise, and grumpy lunar animations, this is the most unique alarm clock the universe. Or at least in our solar system!

With a bloat-free approach, Wakey is a dedicated alarm clock app, with no added features (timer, stopwatch, etc)

Wakey’s different alarm states UI (snooze, default and dismiss — from left to right)