SOLID Android analytics with RxJava2

Aris Papadopoulos
ProAndroidDev
Published in
7 min readAug 8, 2017

--

Source: unsplash.com

This article was featured on Android Weekly, Issue #270

Integrating analytics is an indispensable part of the development of a mobile app nowadays. In this article we will discuss how we can implement a flexible analytics framework for our app that follows the SOLID principles, with the help of RxJava.

The main requirements that we will set for such a framework are the following:

  • Independence from any specific analytics platform: There are multiple analytics platform out there (Firebase Analytics, Mixpanel, Amplitude, Clevertap and many others) and our code shouldn’t be coupled to any of them.
  • Flexibility: We might actually want to trace events in multiple analytics platforms, or enable one platform for only a segment of our users, or be able to enable/disable platforms individually and dynamically.
  • Simplicity/Unobtrusiveness: Analytics is a cross-cutting concern for our app, and analytics code tends to “pollute” the application code. Our framework should offer a minimalistic API, so that the application code that calls it remains as simple and short as possible.

The basics: The API of the framework

When dealing with analytics, we are typically interested in two things: Events and User Properties. All analytics platforms offer support for these two concepts.

Events have a name, and can optionally have a number of properties (key-value pairs). Different analytics platforms represent these properties in different formats: some of them accept a JSON object, while others use an android.os.Bundle or a Map. But we don’t actually care about that at the moment. We need to represent the Events in a format that will work well internally and isn’t tied to a specific platform.

We will therefore use a HashMap<String, Object> which is the simplest data structure to use and can be easily transformed to other formats.

User properties are even simpler than that: they are plain key-value pairs, so the class we’ll use looks like this.

Then we need a class that will be the main entry point to the analytics framework. We will call this class Analytics and it will be a singleton. How clients (application code that posts events to the analytics framework) will access this class is a matter of preference. We can either use a plain-old singleton and get the instance with Analytics.getInstance() or use dependency injection (Dagger2).

The Analytics class will offer two methods, one for tracking events and one for updating user properties — and this is pretty much the public API of our framework.

We will discuss the implementations of these methods later.

Creating Events

Imagine we have an event that tracks whenever users save an entity in our app. This event has a boolean parameter that represents whether the operation is actually a create or an edit. The full code to create and track this event would look like this:

This code is 4-lines long and contains references to two constants (the event name and the parameter name). We obviously don’t want all this code in our application code (eg in a button’s ClickListener or in a Presenter).

The solution here is to use factory methods in order to create the events. The idea would be to offer a static factory method for each event we want to track, rather than creating them directly through the Event constructor. This way, the details of creation of the Events are hidden inside the factory methods, and the application code doesn’t need to know these details.

That’s actually the reason why the constructor of Event in the snippet above was declared as package-private: the application code shouldn’t need access to it — only the framework itself should create events, through these factory methods.

In order to keep things clean, we can keep all these factory methods in a dedicated class called Events.

And now the application code is a one-liner and looks a lot cleaner:

Obviously all this applies to User Properties as well. We will be focusing just on Events for the rest of the article.

Sending the Events to the analytics platforms

We’ve seen how we can create events and how we can send them to our Analytics class. We now need to see how to actually send these events to the analytics platforms of our choice.

If we are interested in multiple platform or we want to have flexibility to switch platforms at some point, we need a Tracker interface that will might have various implementations: FirebaseTracker, AmplitudeTracker etc.

Old-school solution

For each concrete tracker we need to take our generic Event, check if the tracker is interested in this event, and if it is, transform the Event into an appropriate format, and then send it to the platform using the platform’s SDK.

The first approach that one would naturally come up with would be to define an AbstractTracker that outlines this algorithm and that declares abstract methods for the parts that are platform-specific to be implemented by the concrete Trackers.

With this approach, our Analytics class would have to keep track of all trackers that are enabled at any moment, and send them the Events that it receives from our application.

This approach might seem OK, and gets its job done, but we can achieve a lot more by using RxJava.

Leveraging the power of RxJava

With RxJava we can have our Analytics class create and expose Observables that emit the Events and User Properties that we generate in our application code. The Trackers can then subscribe and unsubscribe at their will and react accordingly to the Events emitted.

Some of the advantages that this approach has, in comparison to the traditional approach, are the following:

  • Decoupled code. Analytics doesn’t need to hold a list of the active trackers and actually doesn’t need to know anything about them; it just emits the events and its job is over. It’s the trackers’ job at that point to react to the events.
  • Increased flexibility through subscribing/unsubscribing. Each Tracker can subscribe and unsubscribe at any moment. And it can subscribe individually to the Events stream or to the User Properties stream if it’s interested in only one of them. You might want to have a tracker activated for only a segment of your users, or you might want be able to disable tracking through a push notification/remote config if you reach your quota on a platform before the end of the month. With the reactive approach, each Tracker handles all the logic of when to subscribe or unsubscribe on its own; no other component needs to know about it.
  • Enhanced readability. This might be a matter of personal preference, but I find that RxJava code with composed operations is way more readable than procedural code like the one we saw before. It is also more flexible: since rx operations are composable we can more easily customize the algorithm that a Tracker uses to process an Event, without having to mess with the class hierarchy as we would have to do in the template methods approach.

So let’s move to the details of how we would implement this with RxJava. First we need to create the streams; we will use two separate Observables: one for Events and another one for User Properties. This way the Trackers can subscribe selectively to those that they are interested in.

We will actually use PublishSubjects rather than plain Observables: whenever the application code sends an Event to Analytics, we will post it to the PublishSubject , which will in turn forward the Event to the subscribed trackers.

Notice how the eventsStream and propertiesStream methods return an Observable rather than the PublishSubject as it is, because we want to prevent the Trackers from inadvertendly posting events to it.

And here is an example of how a Trackers would look like:

Lots of interesting things are happening here:

  • We have encapsulated the logic of whether the tracker is interested in the Events or in the User Properties in their own methods. These methods might return a static value (if for example we use a specific Tracker only for User Properties) or they might include more complex logic (eg return a value based on Firebase’s RemoteConfig).
  • We have encapsulated the logic for subscribing to each stream in a separate method, and we keep a reference of the Disposable in a field. This way we can unsubscribe anytime from the stream if necessary, and we can re-subscribe at a later point by calling the method subscribeToEvents again.
  • The steps of the algorithm for processing the Events is nicely expressed in terms of composed RxJava operators, each of which only takes up one line, with the help of method references. It takes just a glance to understand what’s going on, and the algorithm could be easily customized or extended by adding additional steps.

Conclusion

Analytics is a necessary part of every app, but it’s hardly the most interesting or fun part to write. Hopefully, what we’ve seen in this article can make working with Analytics a bit less tedious; it certainly did so for me.

And although the article title makes reference to the SOLID principles, we haven’t offered explicit justification about how our design decisions are in line with these principles. This is left as an exercise to the reader…

--

--

Android Software Engineer. RxJava, Kotlin, SOLID, Clean code, Clean Architecture and other cool stuff.