Jetpack Compose: Passing data implicitly in Composable screens

Saqib
ProAndroidDev
Published in
6 min readJan 19, 2024

--

What is CompositionLocal ? when/how can we use it? How to pass widely used data between different composable screens? We will answer such questions in this story.

Image taken from Android Developers blog — Whats new in Jetpack Compose

CompositionLocal is used to pass data through Composables implicitly. To better understand we need to see how Composable functions pass data in general.

Composable functions pass data through UI tree via explicit parameters being passed into other descendants Composable functions. But if the data which is widely and frequently used and its being pass via parameters to descendants Composable functions down in the data tree, it becomes unmanageable, bulky and inconvenient to pass such data in this way.

Compose provides CompositionLocal to handle such cases where data will be available to the UI tree implicitly. CompositionLocal will be provided a value in a node of the Compose UI tree and will be available to all the descendants Composable functions implicitly without being passed it as a parameter explicitly.

CompositionLocal element is usually prefixed with Local to allow better discoverability with auto complete and you get the value of the CompositionLocal via the current property of the CompositionLocal .

CompositionLocal allows to provide different values at different levels of the Composition tree, so current property will correspond to the closest value provided by an ancestor. CompositionLocal scope the value to a Composable UI tree.

If you type Local in any Composable then Android Studio will show a list of all Local objects or constants available in Composable . You might be familiar with LocalContext which provides the current context which you get by calling LocalContext.current .

MaterialTheme uses CompositionLocal under the hood. MaterialTheme object provides colorScheme, shapes and typography properties via CompositionLocal objects created for each of them respectively.

Looking at the MaterialTheme object.

MaterialTheme object is exposing properties colorScheme, typography and shapes which under the hood are provided via CompositionLocal objects LocalColorScheme , LocalTypography and LocalShapes created for each of these respectively.

We use MaterialTheme in our MainActivity while providing our App Main Composable as a content lambda inside MaterialTheme composable. Whenever we create a Jetpack Compose project a custom Theme class is generated which takes the content lambda and updates the MaterialTheme properties.

Looking at the code above MaterialTheme is not being pass as a parameter to Greeting composable function but its still available/accessible and we are using colorScheme and typography properties of MaterialTheme inside Greeting composable because MaterialTheme is using CompositionLocal under the hood and its implicitly available to the Greeting composable.

What are the signals for using CompositionLocal?

When data is widely used within Composable UI tree i.e It’s not used in a few Composables but rather is used in the most of the Composables. Passing that data via parameters will create a lot of overhead so it will be better to use CompositionLocal .

When deciding about CompositionLocal you would need to make sure it has a default value which can be used as a default in certain situations e.g in writing Tests or previews of Composable. Otherwise you would need to provide values explicitly which is unnecessary and not clean.

What are ways to create CompositionLocal?

There are two ways to create CompositionLocal

  1. compositionLocalOf — when value changes then during recomposition only the content which reads its value will be recomposed.
  2. staticCompositionLocalOf — unlike compositionLocalOf the whole content will need to recompose if its value changes. If the value does not change more often it will be better to use staticCompositionLocalOf for better performance.

Example

Let’s take an example to show usage of CompositionLocal . I will share the Github link at the bottom of the article.

Let’s say we want to log an analytics event when any screen composable gets visible the first time. To achieve that we will have analytics logging implementation overriding some interface e.g AnalyticsLogger. As we want to use AnalyticsLogger inside composable so we need to create a CompositionLocal for AnalyticsLogger . That means we need to provide default dummy behaviour for the cases for unit tests and preview.

Below showing the analytics related code which is nothing new only need to pay attention on DummyAnalyticsLogger which just overrides AnalyticsLogger interface providing no implementation. This will be used to provide default implementation for CompositionLocal created for AnalyticsLogger .

Creating CompositionLocal for AnalyticsLogger.

As AnalyticsLogger will not change once its implementation is assigned to CompositionLocal, a better choice will be to use staticCompositionLocalOf Api to create avoiding performance overhead.

  • LocalAnalyticsLogger created with prefix Local, as it's recommended to Local as prefix to CompositionLocal for better discoverability.
  • Passing DummyAnalyticsLogger() as default implementation for LocalAnalyticsLogger .
  • A composable helper function LogScreenVisited created which is using LocalAnalyticsLogger to access AnalyticsLogger
  • Every CompositionLocal provides current property, using that will give access to the value of CompositionLocal e.g in our case LocalAnalyticsLogger.current was used inside LogScreenVisited composable.

Providing value to the CompositionLocal

We can assign value to CompositionLocal using Api CompositionLocalProvider .

CompositionLocalProvider takes CompositionLocal and value as parameters and exposes a content lambda which we use to provide the node of the Compose UI tree, that means we will scope this CompositionLocal to the UI tree starting with that node.

Let’s provide analyticsLogger created from Hilt as value to LocalAnalyticsLogger using CompositionLocalProvider .

In above code CompositionLocalProvider is assigning value to LocalAnalyticsLogger, now when any descendant Composable in the hierarchy accesses LocalAnalyticsLogger.current it will have access to the analyticsLogger bind inside Hilt Module .

Any node(composable function) can change the value of CompositionLocal providing new value using CompositionLocalProvider , so for the composable functions down in that hierarchy will access the latest value assigned to the CompositionLocal which is the value provided by the closest ancestor.

Usage of CompositionLocal

In our case we will call LogScreenViewed composable inside screen composable which internally access LocalAnalyticsLogger.current to log screen view events as shown in the code below.

Pay Attention when creating CompositionLocal

The CompositionLocal should not be used when we are unable to provide default value to it otherwise it will make it difficult to manage especially in Tests or previews and you would need to provide explicit values to it making it unclean and hard to manage.

CompositionLocal should only be used if the value can be scoped to a UI tree and it’s used by most of the composables in the UI tree, if it’s only accessed by a few composable functions it might not be an ideal choice.

Avoid overusing CompositionLocal e.g you should not use it for viewModels to pass viewModel to all the Composable functions down in the hierarchy , its bad practice. It will break the principle of State flow down and events flow up as not all the composable down in the hierarchy need to know about the View Model, only pass the required information to the ViewModel what they required which make the Composable reusable and easier to Test.

CompositionLocal makes the composable hard to reason about, as they create implicit dependencies and the caller of the composable who is using it needs to make sure to satisfy the value for CompositionLocal .

CompositionLocal makes debugging hard, as CompositionLocal provides the ability to change its value by any node in the tree, so while debugging we need to see where the closest value is assigned to the CompositionLocal .

That’s it for now, full code for this example is available below.

I will be looking forward to any comments or suggestions.

Github

Remember to follow for more stories and 👏 if you liked it :)

— — — — — — — — — — —

GitHub | LinkedIn | Twitter

--

--

Senior Mobile Engineer (Android & iOS) , Berlin | Sharing my development experience