ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

An amazing analytics architecture for Android apps: Part 1

Mikhail Kulaha
ProAndroidDev
Published in
8 min readMay 22, 2020

--

If you have ever implemented analytics on Android you know how hard it can be to attach it to the existing app structure. Everything looks more or less fine as long as the number of events doesn’t go over a certain number.

However, once the quantity of events starts to increase, support becomes really difficult. Events start to stick out from places where they shouldn’t be, properties start to disappear and everything becomes a huge mess. Any changes in business logic break analytics and changes in analytics break business logic. Maintaining analytics becomes more and more difficult…

If you’re currently experiencing the same problems and have decided to carry out a refactoring or you planned to implement analytics from scratch, this article is for you.

Fortunately, it’s not so hard to create a good analytics architecture for a mobile app!

In this article I’ll give you an example of a good analytics structure. We’ll see how analytics should look like and later try to implement this structure in a simple app.

As well we’ll talk about how analytics can be structured in a multi module app.

This is the Part 1 of this article in which I’ll explain how good analytics structure should look like. Part 2 will be ready soon. There we’ll take a look at a simple app and try to implement this structure there.

Ok, let’s start!

Part 1 . An amazing analytics architecture

What is an AMAZING analytics architecture, anyway?

It’s much easier to answer this question by telling you what a BAD analytics implementation looks like:

  • It takes a lot of time to add new events or remove the old ones. Once we change something in analytics the app stops compiling, tests start to fail, Kraken awakens. Reason — we mixed up app and analytics logic
  • It takes even more time to add the new provider for analytics — let’s say your PM has just found a new trendy analytics provider, but it takes too much time for you to implement it. Reason — we bound our analytics to a specific provider which causes a lot of problems with other providers.
  • Analytics logic is very unstable. Sometimes we receive one set of parameters, sometimes another. It takes a lot of time to check what is wrong and why. Reason — we don’t have a strict contract for events and their properties — we can send anything to anywhere, there’s no one to check

Once we know how BAD analytics can look like, it’s much easier to tell how AMAZING analytics should look like:

  1. Analytics events should be decoupled from the existing app structure. This means that if we decide to remove analytics, everything else should remain intact. No logic should be broken up.
  2. Events should have strict parameters and types. They should be idiot-proof — you should not be able to send anything that is not documented. If there’s a set of strings- only these strings should be accepted. If the property type is integer — then only integers, etc.
  3. Business logic of analytics should not interfere with the business logic of the app. Changes in analytics should not fail the tests or anything inside the app.
  4. You shouldn’t have access to analytics fields if you don’t need them. Let’s say you have a module responsible for profile — why do you need to know anything about news feed or camera analytics then?
  5. Analytics logic should not depend on analytics providers. If we decide to remove or add a new provider — it should be done with as little code changes as possible.

Considering these points, we came up with the solution of a layered structure.

Approximate analytics structure

As you can see on this diagram, we have 3 layers.

  • Constants layer
  • Events layer
  • Features layer

Events layer doesn’t know anything about features layer, and features layer doesn’t know anything about constants layer. Let’s look at each layer in more detail.

1 : Constants layer.

In this layer we keep a list of constants — event names and their properties. If a property has a limited set of possible values, we also define them here.

The point of this layer is to set a limit for field names and structure them. We can’t use any constants outside of this layer.

Why? Because, otherwise we wouldn’t know from where this constant came from. It would be much harder to maintain and synchronise everything with our analytics documentation.

For example, our list of constants can look like this:

Why don’t we split these constants into different modules so that each module has its own constants? It’s just easier to handle all constants in one place. Usually event names and fields come from a single document, so it’s much easier to keep track of all updates and changes.

2: Events layer.

In this layer we specify all possible events and declare all possible parameters with their types of events. We also set a list of providers to which these events should be sent.

This layer adds consistency and contracts to our structure. Once we want to send an event, we’ll send it as an object with exact values, not as a map or other collection with pretty vague properties.

That’s how the signature of event can look like:

class CustomEvent( time: Int, name: String) = …

We just can’t send anything else in parameters and have a wrong format!

To implement this idea we will use the following :

  • Base abstract event class which will be extended by other events
  • Event classes which will extend base class and specify what exactly this event should send
  • Event sender — a class which will be sending these events

Base abstract class will look like this

Here we specify the contract, which every event should follow. We have eventName and a map of parameters, where key — is a constant field from Constants layer and a value is whatever we need to send . Once we extend this class, we should set these properties accordingly.

For example, take a look at this event:

Or this:

Once we extend AnalyticsEvent, we should set an event name and a map of the parameters.

eventName and keys for the map will be taken from AnalyticsConstants.

Finally, our event sender will look like this:

AnalyticsFirebaseDummy and AnalyticsAmplitudeDummy are our dummy providers. In real life, we will use real providers instead — Firebase and Amplitude respectively, but it’s up to you which providers you use — it can also be your own analytics.

As you can see APIs of these providers are different — Firebase accepts bundle, Amplitude accepts JSON.

With user properties it is also quite different — for Firebase it is just a String, for Amplitude — a JSON from a list of properties.

That’s why it’s important to not depend directly on providers.

Sending an event

In order to send analytics event we need to create an object with the required parameters, and send it through event sender.

This is how it looks like :

eventSender.sendEvent(
AppEvents.Navigated(
from = AppEvents.Navigated.NavigationSource.HOME,
to = AppEvents.Navigated.NavigationSource.DASHBOARD
)
)

Setting a user property:

The workflow for user properties looks almost the same as for events: we have an abstract base class, and other classes of user properties which extend it.

But instead of splitting user properties into different modules, we’ll keep all of them in the core-analytics module. Why? Because these properties are coming with every event, so there’s no point in splitting them.

Base event property:

The difference with AnalyticsEvent is that instead of a map of parameters we have only one parameter, because we manipulate with only one key-value pair.

The user property itself looks like this:

The sender of user property:

We can set user property by using almost the same call as we did with sending events:

analyticsSender.setUserProperty(UserProperties.NotificationProperty(newState))

That’s it for events layer

3 : Features layer.

This layer is tightly connected to the features of the app. Each feature should only depend on analytics events which this feature is responsible for. For example, dashboard feature will only be dependent for the dashboard events, notifications — only for notification events, etc.

But why is it important to not depend on other events? What difference does it make?” — you ask. First, decoupled modules speed up the build, second, it makes the code cleaner and less error-prone.

In order to bind feature business logic with analytics logic we’ll use analytics interactor. This is the class, which will be injected into ViewModel. ViewModel will be bound to the fragment.

This is how ViewModel looks like:

Instead of being responsible for analytics directly, viewModel delegates everything related with analytics to analytics interactor. We can replace this interactor with mocked data, or remove it completely — nothing will be broken.

This is how interactor looks like:

Analytics interactor should be fully responsible for any analytics logic which should be done. Furthermore, this code becomes very easy to test. In this example we are counting time in seconds between clicks and send SendClicked event.

But what if the same event is sent by other features? What if SendClicked event is sent from another screen? Then we move this event into a shared module and share between different features — the same as we do with shared features.

And that’s it! We went through layered analytics structure inside our app!!

As you can see it’s not so hard to decouple analytics from business logic. Even though this structure looks like an overkill for a small app, once the app starts to grow and the event number is over 100, this structure would be very easy to maintain.

That’s it for Part 1.

Part 2 will be available soon. In it we’ll look at an example app with three screens and try to implement analytics inside it. Will be a lot of fun!

Please subscribe if you don’t want to miss it 🤗. As well, don’t forget to clap 👏 50 times if you really liked this article.

Note: If you disagree with anything in this article or think something can be improved, I would really love to hear your thoughts about it! You can write them in comments or send me a tweet. Thank you very much for that!

Link to the example app with this analytics structure:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (9)

Write a response