State of Compose 2023 results are in! Click here to learn more
Published on

How to use CompositionLocal to implement Analytics in Jetpack Compose

Authors

Each composable function accepts data from its function parameters. As analytics are usually instantiated ahead of time of tracking, we need to pass down the analytics instance from the top-level (screen) composable down to the composable function where the tracking takes place. This means that every in between composable function will have an extra analytics function parameter, only so that they can pass it downwards to the composable that will actually use it.

This blog will show you how to utilize CompositionLocals in order to gain access to the Analytics object of your app from any composable function without polluting your composable functions with unneeded parameters.

What is CompositionLocals

CompositionLocal is a Jetpack Compose API which allow you to access data from any composable function without passing them via the function's parameters. You have probably already used a CompositionLocal (with a bit of syntactic sugar on top) while using theme values in your Jetpack Compose app. What the MaterialTheme object does is providing easy access to your application's theme from any composable function.

We can use the same technique to implement analytics in your app.

Basic setup

For simplicity shake our analytics consists of a single tracking function. It is defined by the following interface:

interface Analytics {
    fun logEvent(eventName: String)
}

We will need two implementations of the interface. One will be used as the default implementation:

class LoggingAnalytics: Analytics {
    override fun logEvent(eventName: String) {
        println(eventName)
    }
}

while the other will use the analytics framework of your choice:

class FirebaseAnalytics: Analytics {
    override fun logEvent(eventName: String) {
        Firebase.analytics.logEvent(eventName)
    }
}

Let's create our CompositionLocal. As we are not expecting the Analytics object to be updated, we will use the staticCompositionLocalOf { } to create it.

The CompositionLocal needs to be created outside of any activity or object, as it needs to be visible from any composable function:

val LocalAnalytics = staticCompositionLocalOf<Analytics> {
    LoggingAnalytics()
}

In our top level composable, wrap our content with a CompositionLocalProvider. As at this point we are either in an Activity or Fragment we can inject the Analytics of our choice (using Hilt or any other dependency injection frameworks):

val analytics = FirebaseAnalytics()
setContent {
    AppTheme {
        CompositionLocalProvider(LocalAnalytics provides analytics) {
            HomeScreen()
        }
    }
}

you can now use Analytics from any composable function wrapped by the CompositionLocalProvider:

@Composable
fun HomeScreen(viewModel : MyViewModel = hiltViewModel()) {
    val state =
    val analytics = LocalAnalytics.current
    Button(onClick = { analytics.logEvent("buttonClick") }) {
        Text("Click me!")
    }
}

you can now access analytics from any composable function deep in your Composition tree 👏

Be aware of the downsides and code clarity

Just because you can use CompositionLocal to access Analytics from any composable function, it does not mean you should. The above example showcases how to use the CompositionLocal API so that you do not need to pass the Analytics instance down via parameters.

Using a CompositionLocal creates implicit dependencies and it can make your app harder to reason and maintain. You still need to be cautious to access analytics from the right layer of your app such as a screen level composable function instead of a single element (such as buttons).

CompositionLocal is a powerful API that has its usages such as the case described. In fact, Jetpack Compose makes heavy use of it. However, you need to be aware that it is easy to abuse and can mess your project's code clarify if not careful.

As you cannot be certain of the current value of the CompositionLocal, it can make your code hard to read and follow. This is because it depends on the value passed to the CompositionLocalProvider in an other place of your app. This makes code harder to debug as you will have to find in which place of your app the value of the CompositionLocal was modified.