ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Unifying Your Android App’s Analytics: A Provider-Agnostic Approach with Jetpack Compose

Kartik Arora
ProAndroidDev
Published in
6 min readFeb 14, 2025

In the dynamic world of mobile app development, understanding user behaviour and fine-tuning your app is paramount. This is where analytics come into play, offering invaluable insights into how users interact with your app. However, implementing analytics often presents a tangled web of challenges: vendor lock-in, clunky SDKs, and the ever-evolving landscape of Android development, particularly with Jetpack Compose.

This article delves into a flexible, provider-agnostic approach to streamline analytics integration in Jetpack Compose apps, leveraging smart design patterns and Compose’s unique features.

The Common Analytics Hurdles

Let’s face it: dealing with analytics can be a real headache. You might find yourself grappling with these common issues:

  • Provider Lock-in: Your app’s analytics are tightly coupled to a specific provider, making switching a nightmare of code rewrites.
  • Managing Multiple Providers: You need to send data to multiple analytics platforms, leading to code duplication and messy logic.

Taming the Beast: Abstraction and the Composite Pattern

Abstraction is our knight in shining armor against vendor lock-in. Imagine defining a simple interface that outlines the essential analytics actions your app needs, like tracking screen views (trackScreen) and user actions (trackAction). This interface acts as a middleman, decoupling your core app code from the nitty-gritty of specific analytics providers.

Let’s visualize it with some code! Imagine an interface like this:

interface AnalyticsInterface {
fun trackScreen(screenName: String)
fun trackAction(actionName: String)
}

Now, you can create concrete implementations of this interface for different providers. For instance, here’s a Firebase implementation:

class FirebaseAnalyticsImpl() : AnalyticsInterface {
// FirebaseAnalytics instance

override fun trackScreen(screenName: String) {
// Convert Event.View to Firebase parameters and log the event
}

override fun trackAction(actionName: String) {
// Convert Event.Action to Firebase parameters and log the event
}
}

To tackle the multiple provider conundrum, the composite pattern swoops in. Think of it like a central command center that orchestrates multiple devices. A composite class holds a collection of these AnalyticsInterface implementations and delegates calls to each of them.

class CompositeAnalytics(
private val providers: List<AnalyticsInterface>
) : AnalyticsInterface {
override fun trackScreen(screenName: String) {
providers.forEach { provider ->
provider.trackScreen(screenName)
}
}

override fun trackAction(actionName: String) {
providers.forEach { provider ->
provider.trackAction(actionName)
}
}
}

Let’s say you have implementations for Firebase, Adobe, and Mixpanel analytics. Creating a composite setup would look like this:

val firebaseImpl = FirebaseImpl()
val adobeImpl = AdobeImpl()
val mixpanelImpl = MixpanelImpl()

val compositeAnalytics = CompositeAnalytics(
listOf(firebaseImpl, adobeImpl, mixpanelImpl)
)
compositeAnalytics.trackScreen("HomeScreen")
compositeAnalytics.trackAction("ButtonClicked")

In this scenario, a single call to trackScreen on the compositeAnalytics object would simultaneously send the screen view event to Firebase, Adobe, and Mixpanel.

The composite pattern empowers you to build a flexible and maintainable analytics system that adapts to changing requirements and provider preferences.

Embracing Jetpack Compose: LaunchedEffect and CompositionLocal

Jetpack Compose revolutionizes Android UI development with its declarative approach, moving away from the traditional imperative style. This shift brings new opportunities and challenges, particularly when it comes to integrating analytics. We can no easily use the familiar Android lifecycle methods like onResume and onPause. They do exist in the compose world, however it isn’t as easy as overriding the functions. Instead, we leverage Jetpack Compose features like LaunchedEffect and Composition Locals.

LaunchedEffect enables us to perform side effects, such as sending analytics data. Its strength lies in executing a code block only once during the initial composition of a composable function and optionally again when specified dependencies change.

To illustrate, let’s imagine tracking a screen view every time it appears. With LaunchedEffect, we achieve this as follows:

@Composable
fun MyScreen(screenName: String, analytics: AnalyticsInterface) {
LaunchedEffect(Unit) { // Triggered only once upon initial composition
analytics.trackScreen(screenName)
}
// ...The rest of your composable content...
}

Passing the analytics object through each layer of our composable functions can quickly become unwieldy. Composition Locals come to the rescue, acting as invisible data containers accessible to any composable function within a defined scope. We can provide the AnalyticsInterface through a Composition Local, eliminating the need for manual passing.

Let’s break down the setup:

1. Creating a Composition Local:

val LocalAnalytics = compositionLocalOf<AnalyticsInterface> {
error("No analytics provided!")
}

We define a Composition Local named LocalAnalytics designed to hold an AnalyticsInterface. The error function safeguards against instances where the AnalyticsInterface is not provided, causing a crash.

2. Providing the Analytics Implementation:

@Composable
fun MyApp(analytics: AnalyticsInterface) {
CompositionLocalProvider(LocalAnalytics provides analytics) {
// ... Your app's composables ...
}
}

We wrap our top-level composable, likely residing in your Activity or root composable, with CompositionLocalProvider. This makes the actual analytics implementation accessible to all child composables within the defined scope.

3. Accessing Analytics in Any Composable:

@Composable
fun SomeDeeplyNestedComposable() {
val analytics = LocalAnalytics.current
analytics.trackAction("ButtonClicked")
// ... The remaining composable content ...
}

Even from a deeply nested composable, we can directly obtain the AnalyticsInterface via LocalAnalytics.current. This allows us to track events without the overhead of manual parameter passing.

In essence, Jetpack Compose provides a fresh perspective on integrating analytics into our applications. Features like LaunchedEffect and CompositionLocal offer elegant solutions to the challenges presented by this new paradigm.

What if you could skip the heavy lifting and focus on what truly matters: building an amazing app?

Introducing Anylytics

Anylytics is an intuitive, open-source Android library designed to make analytics integration in Jetpack Compose applications a breeze.

Here’s why you should choose Anylytics:

  • Say Goodbye to Vendor Lock-in: Migrating between analytics providers becomes effortless. Whether you’re using Firebase Analytics, Adobe Analytics, or considering other options, Anylytics adapts to your choices, ensuring a smooth transition without code rewrites.
  • Seamless Integration with Jetpack Compose: Anylytics leverages Composition Locals, a powerful feature in Jetpack Compose. Access your analytics implementation from any composable function, eliminating the need to pass instances through multiple layers of your UI.
  • Simplicity and Ease of Use: Anylytics offers a concise and intuitive API. Tracking screen views, user actions, and capturing rich context data is as simple as a few lines of code.
  • Support for Multiple Analytics Providers: Send data to multiple providers simultaneously using Anylytics’ built-in support for the Composite pattern.

Anylytics is your solution — an intuitive, open-source Android library that simplifies analytics integration in Jetpack Compose. Stop wrestling with SDKs and start focusing on building exceptional user experiences.

Let’s see Anylytics in action:

Implementation:

  1. Define Your Analytics Interface: Anylytics provides the AnylyticsInterface with functions like trackScreen and trackAction. Implement this interface for each analytics provider you intend to use.
  2. Create Provider-Specific Implementations: Create concrete implementations of the AnylyticsInterface for your chosen providers.
  3. Use the CompositionLocalProvider : In your main Activity or root composable, utilise CompositionLocalProviderto make your analytics implementation accessible throughout your application.
  4. Track Events Effortlessly: Inside any composable, access the analytics implementation using LocalAnylyticsInterace.current, and log events using functions like trackScreen and trackAction.

Data Classes: The Heart of Anylytics

Anylytics uses data classes to structure analytics data for clarity and ease of use.

  • Event: Represents different types of events, such as screen views (Event.View) and user actions (Event.Action).
  • ContextData: Holds additional contextual information related to an event, including a screen name and a mutable map for key-value pairs.
  • Breadcrumbs: Captures user navigation flow through your app, storing information about the current section, subsection, and sub-subsection.

Example Usage:

val screenViewEvent = Event.View(
screenName = "HomeScreen",
contextData = ContextData(
screenName = "HomeScreen",
contextMap = mutableMapOf("item_id" to "123")
),
breadCrumbs = BreadCrumbs(
section = "Home",
subSection = "Products",
subSubSection = "Details"
)
)
analytics.trackScreen(screenViewEvent)

Current State and Future Roadmap

Anylytics, currently in its early stages, is available on GitHub and Maven Central. The library includes artifacts for Firebase Analytics and Adobe Analytics. The roadmap includes:

  • Improving API documentation.
  • Exploring multiplatform support for wider adoption beyond Android.

Anylytics is more than just an analytics library - it is a step towards a simpler, more adaptable analytics integration experience for Jetpack Compose.

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 (1)

Write a response

I would have made the interface suspend function running on io thread so it doesn't block the main thread for analytics

--