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.
CompositionLocal
element is usually prefixed withLocal
to allow better discoverability with auto complete and you get the value of theCompositionLocal
via thecurrent
property of theCompositionLocal
.
CompositionLocal
allows to provide different values at different levels of the Composition tree, socurrent
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.
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 duringrecomposition
only the content which reads its value will be recomposed.staticCompositionLocalOf
— unlikecompositionLocalOf
the whole content will need to recompose if its value changes. If the value does not change more often it will be better to usestaticCompositionLocalOf
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 prefixLocal,
as it’s recommended toLocal
as prefix toCompositionLocal
for better discoverability.- Passing
DummyAnalyticsLogger()
as default implementation forLocalAnalyticsLogger
. - A composable helper function
LogScreenVisited
created which is usingLocalAnalyticsLogger
to accessAnalyticsLogger
- Every
CompositionLocal
providescurrent
property, using that will give access to the value ofCompositionLocal
e.g in our caseLocalAnalyticsLogger.current
was used insideLogScreenVisited
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() | |
} | |
} | |
} |
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