Blog Infos
Author
Published
Topics
, , , ,
Author
Published

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 colorSchemeshapes and typography properties via CompositionLocal objects created for each of them respectively.

Looking at the MaterialTheme object.

object MaterialTheme {
/**
* Retrieves the current [ColorScheme] at the call site's position in the hierarchy.
*/
val colorScheme: ColorScheme
@Composable
@ReadOnlyComposable
get() = LocalColorScheme.current
/**
* Retrieves the current [Typography] at the call site's position in the hierarchy.
*/
val typography: Typography
@Composable
@ReadOnlyComposable
get() = LocalTypography.current
/**
* Retrieves the current [Shapes] at the call site's position in the hierarchy.
*/
val shapes: Shapes
@Composable
@ReadOnlyComposable
get() = LocalShapes.current
}

MaterialTheme object is exposing properties colorSchemetypography 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.

////*** Setting content in MainActivity
CompositionLocalSampleTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
////*** Greating Composable
@Composable
fun Greeting(name: String) {
Text(
modifier = modifier,
text = "Hello $name!",
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge,
)
}

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 .

// AnalyticsLogger interface
interface AnalyticsLogger {
fun logEvent(name: String, params: List<AnalyticsParam>)
}
data class AnalyticsParam(val name: String, val value: String)
// extension method to log screen visit event
fun AnalyticsLogger.logScreenVisited(screenName: String) {
logEvent("screenViewed", listOf(
AnalyticsParam("screen_name", screenName)
))
}
// DummyAnalyticsLogger
// Its created to provide default value to the CompositionLocal
class DummyAnalyticsLogger: AnalyticsLogger {
override fun logEvent(name: String, params: List<AnalyticsParam>) = Unit
}
// Actual Implementation for logging analytics, It will be bind using Hilt
class AnalyticsLoggerImpl @Inject constructor(
) : AnalyticsLogger {
override fun logEvent(name: String, params: List<AnalyticsParam>) {
// actual implementation via firebase or newrelic or any other
}
}
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.

// compositionLocal created for AnalyticsLogger
val LocalAnalyticsLogger = staticCompositionLocalOf<AnalyticsLogger> {
DummyAnalyticsLogger()
}
//a helper compsable function created which is using compositionLocal LocalAnalyticsLogger
// created above and assigning default value
// ocalAnalyticsLogger.current gets current assigned value of AnalyticsLogger which will be
// DummyAnalyticsLogger unless a new implementation is provided
@Composable
fun LogScreenVisited(
screenName: String,
analyticsLogger: AnalyticsLogger = LocalAnalyticsLogger.current
) = LaunchedEffect(Unit) {
analyticsLogger.logScreenVisited(screenName)
}
  • 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 .

// analyticsLogger created inside MainActivity using hilt,
// It will take actual implementation binded inside Hilt Module
@Inject
protected lateinit var analyticsLogger: AnalyticsLogger
// Assigning value inside setContent lambda of MainActivity
CompositionLocalProvider(LocalAnalyticsLogger provides analyticsLogger) {
CompositionLocalSampleTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WelcomeScreen()
}
}
}

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

No results found.

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.

@Composable
fun WelcomeScreen() {
Column {
Text(
text = "Hello World!",
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge,
)
}
LogScreenVisited(screenName = "Welcome")
}
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.

https://github.com/saqib-github-commits/CompositionLocalSample?source=post_page—–73d88b7559c3——————————–

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

— — — — — — — — — — —

GitHub | LinkedIn | Twitter

This blog is previously published on proanroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu