Writing SOLID analytics with Kotlin

Helder Pinhal
Aug 14 2020
Posted in Engineering & Technology

Refactoring cluttered analytics

When building mobile apps, you are often required to implement analytics and, in some cases, to support multiple analytics providers. Analytics tracking alone can easily pollute your logic code but when you need to add that multi-provider support, things quickly get out of hand.

Taking a quick look at an example, you can see how cluttered an activity can become, even without having to handle event payloads.

class MainActivity : AppCompatActivity() {

    ...

    private fun onFeedbackGiven() {
        Toast.makeText(this, "Thank you for your feedback!", Toast.LENGTH_LONG).show()

        // Report to Fabric
        Answers.getInstance().logCustom(
            CustomEvent("feedback_given")
        )

        // Report to Firebase
        FirebaseAnalytics.getInstance(this).logEvent("feedback_given", null)
    }
}

With the example above, we have a very tightly couple solution. Should we need to add or remove a provider, we would have to go through each activity, fragment, etc., in order to make our changes.

Wouldn't it be nice to report those events in an unified, provider-agnostic one-liner way? Let's consider a hub-like approach for this scenario.

Like so, we could replace the code above with the following.

class MainActivity : AppCompatActivity() {

    ...

    private fun onFeedbackGiven() {
        Toast.makeText(this, "Thank you for your feedback!", Toast.LENGTH_LONG).show()

        // Report to all providers
        analyticsHub.trackEvent(FeedbackGivenEvent())
    }
}

Let's start by setting up our AnalyticsHub. This object will be responsible for dispatching the events to each analytics provider. In short, it will contain a collection of providers, and forward the events to each one of those.

class AnalyticsHub(
    private vararg var providers: Provider
) {

    fun trackContentView(name: String) {
        providers.forEach { it.trackContentView(name) }
    }

    fun trackEvent(event: Event) {
        providers.forEach { it.trackEvent(event) }
    }
}

interface Provider {

    fun trackContentView(name: String)

    fun trackEvent(event: Event)
}

interface Event {

    val eventName: String
}

Creating the events

We should create a specific class, which implements the Event protocol, for each event we want to support. For this example, we'll stick to the feedback example but the same principle applies to pretty much any kind of event — login, purchasing, so on and so forth.

class FeedbackGivenEvent : Event {
    override val eventName = "feedback_given"
}

Creating a provider

Starting with the Fabric implementation, we simply need to create a class that conforms to our Provider protocol and add the Fabric-specific code to track the events.

class AnswersProvider : Provider {

    private val answers = Answers.getInstance()


    override fun trackContentView(name: String) {
        answers.logContentView(
            ContentViewEvent()
                .putContentName(name)
        )
    }

    override fun trackEvent(event: Event) {
        answers.logCustom(
            CustomEvent(event.eventName)
        )
    }
}

Wrapping things up

Lastly, we need to instantiate the AnalyticsHub with our desired providers and we're done. We could have it managed by Dagger for instance, but for simplicity's sake let's create it in our Application.

class PlaygroundApp : Application() {

    lateinit var analyticsHub: AnalyticsHub
        private set


    override fun onCreate() {
        super.onCreate()

        analyticsHub = AnalyticsHub(
            AnswersProvider()
        )
    }
}

We can now call our AnalyticsHub like we wanted. Simple and with minimal impact on business logic.

If we were to add another provider to the mix, say Firebase, we'd only need to create another class conforming to Provider and modify the AnalyticsHub initialization like so.

class FirebaseProvider(
    context: Context
) : Provider {

    private val firebaseAnalytics = FirebaseAnalytics.getInstance(context)


    override fun trackContentView(name: String) {
        firebaseAnalytics.logEvent(FirebaseAnalytics.Event.VIEW_ITEM, Bundle().apply {
            putString(FirebaseAnalytics.Param.VALUE, name)
        })
    }

    override fun trackEvent(event: Event) {
        firebaseAnalytics.logEvent(event.eventName, null)
    }
}

...

analyticsHub = AnalyticsHub(
    AnswersProvider(),
    FirebaseProvider(this)
)

Bonus provider

Notificare goes far beyond marketing messages. You can also leverage our platform to track events, gather all the insights you need in beautiful charts or reports and even run automation workflows based on such events. This is what we call Actionable Analytics, and with it you can easily create automated messages or categorize your users in response to these events.

class NotificareProvider : Provider {

    override fun trackContentView(name: String) {
        Notificare.shared().eventLogger.logCustomEvent(
            "content_view",
            mapOf(
                Pair("view", name)
            )
        )
    }

    override fun trackEvent(event: Event) {
        Notificare.shared().eventLogger.logCustomEvent(event.eventName)
    }
}

That's it. Most of of the work is done. In a real-world app you'd need to create all the events you want to track and hook the hub across your app.

Keep up-to-date with the latest news