The journey to clean code with analytics in MVVM

Csaba Szugyiczki
Supercharge's Digital Product Guide
8 min readSep 10, 2020

Planning and implementing analytics measurements is really important if you want to know how your users interact with your application. More often than not analytics is only added to an application once the main functionality is ready to be shipped, or even after the first release.

The most common analytics measurements include tracking navigation screen by screen, reporting clicks with or without additional parameters and logging key events like a purchase. But where should we add the code that tracks all of this? Analytics-related code tends to creep into every layer of the application.

Analytics dashboard

At Supercharge we focus on writing code that is clean and easy to maintain. It helps us during development and adding new features or onboarding new team members is also easier this way. Our go-to architecture is MVVM, which became the de facto standard in recent years, supported by AndroidX libraries. On top of that, we also use Data Binding. This prevents us from adding too much redundant code to our Fragments.

The goal of this article is to guide you through our journey of how we came up with a clean way of adding analytics to applications. The article will focus on Android apps written in MVVM, but the final solution can be applied to any platform or architecture.

To demonstrate our solution, I have created an example application, based on the sample from our previous article about Hilt. The app is really simple. It has a TextView that displays the current value of the counter, which can be incremented and decremented with two separate buttons. You can find the full source here.

At first, we define the ViewModel that contains the logic and the state of the screen. As you can see, the current value is in a MutableLiveData field. The increment and decrement events are handled via OnClickListeners bound to the buttons.

We can bind the value field to the TextView, and the OnClickListeners to the buttons using the android:onClick BindingAdapter defined by the Data Binding Library.

Now that we have a functioning application, we can start tracking the usage. Let’s see what steps it took us to get to this sweet spot, where we can separate analytics code from the rest of the app layers and integrate it into the existing code as seamlessly as possible.

Step 0: The base

Having a solid base for your analytics tracking is crucial because it can save you a lot of trouble in the long run. I highly recommend this article as a truly great way to get started. What Mikhail describes here is pretty much the same as we had in the beginning.

In our sample, we have the following Constants. For the sake of simplicity, all of them are Enums — but if it better suits your needs, feel free to create a more sophisticated hierarchy.

We also define a simple AnalyticsTracker interface that can be used to track events, with optional parameters. The default implementation in the sample is only logging them, but it could also send different events to one or more analytics services.

Step 1: The easy way

The easiest way to add analytics tracking to an MVVM app is to inject your tracker implementation directly into your ViewModels and call the appropriate tracking function from your event handlers.

While this works fine, it pollutes our ViewModel with code that does not have to do anything with the main functionality. This was not the way we wanted to take, so we kept looking for other solutions.

Step 2: The BindingAdapter

Since adding more complexity to our ViewModels was not an option, we started to look elsewhere. Usually, our View layer is pretty straightforward, most of theFragments are basically empty classes, specifying only the ViewModel type and the layout to inflate. This is also the layer closest to the events that we are about to track. However, adding tracking directly here is not too practical. This way we would need to:

  • inject the tracker into every Fragment
  • register the Fragments’ OnClickHandlers to every Button
  • track the events
  • proxy the events to the ViewModel

Repeated tasks like these are usually best to be implemented as BindingAdapters.

We already had a BindingAdapter to bind UI events to ViewModels. When we started to use Data Binding, we have borrowed the Command pattern from .NET, which can be used to run code when a specific event happens, same as regular OnClickListeners. However, it is more open for extension, and we are not depending on View-specific implementation details inside our ViewModels. In the simplest form, a Command is just an interface for an executable piece of code.

This can be implemented in Kotlin by passing a function to the constructor, and invoking it when the execute method is called.

Swapping our OnClickListeners in the CounterViewModel with the new FunctionCommands is as easy as changing the type declaration.

This way, we can create a BindingAdapter that registers an OnClickListener for us and calls the execute function of the Command when the user clicks the View.

So far so good, but we still have the tracking code inside our ViewModels. The only difference is that now we have another layer of abstraction between our View and ViewModel layer. Let’s use this to our advantage. We already have Constants defined for our analytics-related events — and if we add them to the BindingAdapter we can define which Command belongs to which event.

Now we can remove the analytics-related code from our ViewModels, and we can bind Commands to any View along with the event information needed for tracking.

This setup works well… as long as you don’t need to pass any additional parameters with the events you want to track. If you need additional information (e.g., the current value when the button was clicked), you would need to add specific BindingAdapters to pass these parameters. Extending this approach was a dead-end for us, so we took a step back and decided to look for another solution.

Step 3: The AnalyticsModel

With Commands being used all over our applications, extending them to track user events clearly seemed like the easiest way for us. We just needed a way to tell when one of the Commands is executed. So, we decided to extend the Command interface with a Flow that emits an object when the Command is executed. You can use Observable if you are using RxJava, or even register a listener for a vanilla implementation. The point is to get notified when the command is executed.

The implementation of this Command now needs to expose a Flow that emits a new event whenever the execute function is called.

All we need to do now is observe the Flows of the Commands and when an event is emitted, we can call our analytics tracker with the desired parameters. But where should we do it? We don’t want to add any complexity to our ViewModel that is not part of the basic functionality of our application. We love how lean our View layer is, and we simply don’t want to pollute it.

Let’s introduce a new layer then, one that can access ViewModel fields and is accessible by the View just like the ViewModel itself. We named this layer AnalyticsModel, since the way it is created and implemented is pretty similar to how ViewModels work. On the other hand, it observes the ViewModel and reacts upon its changes so AnalyticsView might also be an appropriate naming. Even so, from now on I will refer to it as AnalyticsModel.

AnalyticsModel in MVVM architecture

In this architecture, every screen has a Fragment, a ViewModel and an optional AnalyticsModel. The Fragment can interact with the AnalyticsModel directly, which might be helpful if a user interaction that we need to track is not related to any Command in the ViewModel itself.

The AnalyticsModel has a reference to the ViewModel, meaning that all Commands defined by the latter can be observed by the AnalyticsModel. What’s more, all public fields of the ViewModel are available to collect more parameters if needed. Furthermore, we can also inject repositories to gather user or session information that might not even be directly available in the ViewModel.

The AnalyticsModel implementation for the counter screen example is pretty straightforward. We receive the Flows that emit the decrement and increment events and set up the analytics tracking code that sends not only the events themselves, but also the current value of the counter as an additional parameter. Then we merge these events, so we can collect both streams together. Extending this setup to include more event sources or parameters is easy — but what’s more important, all of our tracking code lives in the same layer.

To make all of this work we need some Dagger multibinding magic that provides the AnalyticsModels to the Fragments. We want analytics to be optional, so if a screen does not have an AnalyticsModel everything will work the same as before. But if we do have one, we want it to be available in the screen’s Fragment.

First, we need a qualifier to use with our multibinding.

Then we create our AnalyticsModelProvider map in our Dagger Module for analytics-related dependencies.

Finally, the AnalyticsModelProvider can use these Providers to create new instances of the registered AnalyticsModels, based on their type.

Now we can provide AnalyticsModels for our Fragments, but we leave it optional. If a screen needs tracking, we just create a new AnalyticsModel and register it into the Provider map. The Fragment can inject the AnalyticsModelProvider and gets the AnalyticsModel if needed.

Summary

This was our journey to find a more maintainable way of adding analytics tracking code to our codebase. The end result, similar to tests, does not require us to modify existing code. All of the tracking code is separated in its own layer. Adding new events is easy. Removing the tracking is even easier: we just take out the AnalyticsModel from the multibinding map.

However, keep in mind that (as with anything else in software) there is no silver bullet. This architecture might be too complex for your needs or it might not meet some of the requirements your specific use case needs. In some of our applications we have also decided to stop at Step 1 and just added the tracking code directly into our ViewModels. It is still fairly easy to disable tracking if you have a solid base, like the one we described in step 0. Yet if you don’t want to have any code in your ViewModels apart from your core functionality, this journey is for you to take.

At Supercharge, we are a next-generation innovation agency working with our clients to create transformative digital solutions. If you liked this article, check out some of Supercharge’s other articles on our blog, or follow us on LinkedIn, and Facebook. If you’re interested in open positions, follow this link.

--

--