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.

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.
CompositionLocalelement is usually prefixed withLocalto allow better discoverability with auto complete and you get the value of theCompositionLocalvia thecurrentproperty of theCompositionLocal.
CompositionLocalallows to provide different values at different levels of the Composition tree, socurrentproperty 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.
| 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 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.
| ////*** 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
compositionLocalOf— when value changes then duringrecompositiononly the content which reads its value will be recomposed.staticCompositionLocalOf— unlikecompositionLocalOfthe whole content will need to recompose if its value changes. If the value does not change more often it will be better to usestaticCompositionLocalOffor 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) | |
| } |
LocalAnalyticsLoggercreated with prefixLocal,as it’s recommended toLocalas prefix toCompositionLocalfor better discoverability.- Passing
DummyAnalyticsLogger()as default implementation forLocalAnalyticsLogger. - A composable helper function
LogScreenVisitedcreated which is usingLocalAnalyticsLoggerto accessAnalyticsLogger - Every
CompositionLocalprovidescurrentproperty, using that will give access to the value ofCompositionLocale.g in our caseLocalAnalyticsLogger.currentwas used insideLogScreenVisitedcomposable.
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() | |
| } | |
| } | |
| } |
Job Offers
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.
Remember to follow for more stories and 👏 if you liked it 🙂
— — — — — — — — — — —
This blog is previously published on proanroiddev.com



