CompositionLocal
is a tool for
passing data down through the Composition implicitly. In this page, you'll
learn what a CompositionLocal
is in more detail, how to create your own
CompositionLocal
, and know if a CompositionLocal
is a good solution for
your use case.
Introducing CompositionLocal
Usually in Compose, data flows down through the UI tree as parameters to each composable function. This makes a composable’s dependencies explicit. This can however be cumbersome for data that is very frequently and widely used such as colors or type styles. See the following example:
@Composable fun MyApp() { // Theme information tends to be defined near the root of the application val colors = colors() } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = colors.onPrimary // ← need to access colors here ) }
To support not needing to pass the colors as an explicit parameter dependency to
most composables, Compose offers CompositionLocal
which allows you
to create tree-scoped named objects that can be used as an implicit way to have
data flow through the UI tree.
CompositionLocal
elements are usually provided with a value in a certain node
of the UI tree. That value can be used by its composable descendants without
declaring the CompositionLocal
as a parameter in the composable function.
CompositionLocal
is what the Material theme uses under the hood.
MaterialTheme
is
an object that provides three CompositionLocal
instances: colorScheme
,
typography
and shapes
, allowing you to retrieve them later in any descendant
part of the Composition.
Specifically, these are the LocalColorScheme
, LocalShapes
, and
LocalTypography
properties that you can access through the MaterialTheme
colorScheme
, shapes
, and typography
attributes.
@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colorScheme, typography, and shapes are available // in MaterialTheme's content lambda. // ... content here ... } } // Some composable deep in the hierarchy of MaterialTheme @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, // `primary` is obtained from MaterialTheme's // LocalColors CompositionLocal color = MaterialTheme.colorScheme.primary ) }
A CompositionLocal
instance is scoped to a part of the Composition so you
can provide different values at different levels of the tree. The current
value
of a CompositionLocal
corresponds to the closest value provided by an
ancestor in that part of the Composition.
To provide a new value to a CompositionLocal
, use the
CompositionLocalProvider
and its provides
infix function that associates a CompositionLocal
key to a value
. The
content
lambda of the CompositionLocalProvider
will get the provided
value when accessing the current
property of the CompositionLocal
. When a
new value is provided, Compose recomposes parts of the Composition that read
the CompositionLocal
.
As an example of this, the LocalContentColor
CompositionLocal
contains the preferred content color used for text and
iconography to ensure it contrasts against the current background color. In the
following example, CompositionLocalProvider
is used to provide different
values for different parts of the Composition.
@Composable fun CompositionLocalExample() { MaterialTheme { // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default // This is to automatically make text and other content contrast to the background // correctly. Surface { Column { Text("Uses Surface's provided content color") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { Text("Primary color provided by LocalContentColor") Text("This Text also uses primary as textColor") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { DescendantExample() } } } } } } @Composable fun DescendantExample() { // CompositionLocalProviders also work across composable functions Text("This Text uses the error color now") }
Figure 1. Preview of the CompositionLocalExample
composable.
In the last example, the CompositionLocal
instances were internally used
by Material composables. To access the current value of a CompositionLocal
,
use its current
property. In the following example, the current Context
value of the LocalContext
CompositionLocal
that is commonly used in Android apps is used to format
the text:
@Composable fun FruitText(fruitSize: Int) { // Get `resources` from the current value of LocalContext val resources = LocalContext.current.resources val fruitText = remember(resources, fruitSize) { resources.getQuantityString(R.plurals.fruit_title, fruitSize) } Text(text = fruitText) }
Creating your own CompositionLocal
CompositionLocal
is a tool for passing data down through the Composition
implicitly.
Another key signal for using CompositionLocal
is when the parameter is
cross-cutting and intermediate layers of implementation should not be aware
it exists, because making those intermediate layers aware would limit the
utility of the composable. For example, querying for Android permissions is
afforded by a CompositionLocal
under the hood. A media picker composable
can add new functionality to access permission-protected content on the
device without changing its API and requiring callers of the media picker to
be aware of this added context used from the environment.
However, CompositionLocal
is not always the best solution. We
discourage overusing CompositionLocal
as it comes with some downsides:
CompositionLocal
makes a composable's behavior harder to reason about. As
they create implicit dependencies, callers of composables that use them need
to make sure that a value for every CompositionLocal
is satisfied.
Furthermore, there might be no clear source of truth for this dependency as it
can mutate in any part of the Composition. Thus, debugging the app when a
problem occurs can be more challenging as you need to navigate up the
Composition to see where the current
value was provided. Tools such as Find
usages in the IDE or the Compose layout inspector provide enough information to
mitigate this issue.
Deciding whether to use CompositionLocal
There are certain conditions that can make CompositionLocal
a good solution
for your use case:
A CompositionLocal
should have a good default value. If there's no default
value, you must guarantee that it is exceedingly difficult for a developer to
get into a situation where a value for the CompositionLocal
isn't provided.
Not providing a default value can cause problems and frustration when creating
tests or previewing a composable that uses that CompositionLocal
will always
require it to be explicitly provided.
Avoid CompositionLocal
for concepts that aren't thought as tree-scoped or
sub-hierarchy scoped. A CompositionLocal
makes sense when it can be
potentially used by any descendant, not by a few of them.
If your use case doesn't meet these requirements, check out the
Alternatives to consider section before creating a
CompositionLocal
.
An example of a bad practice is creating a CompositionLocal
that holds the
ViewModel
of a particular screen so that all composables in that screen can
get a reference to the ViewModel
to perform some logic. This is a bad practice
because not all composables below a particular UI tree need to know about a
ViewModel
. The good practice is to pass to composables only the information
that they need following the pattern that state flows down and events flow up. This approach will make your composables more
reusable and easier to test.
Creating a CompositionLocal
There are two APIs to create a CompositionLocal
:
compositionLocalOf
: Changing the value provided during recomposition invalidates only the content that reads itscurrent
value.staticCompositionLocalOf
: UnlikecompositionLocalOf
, reads of astaticCompositionLocalOf
are not tracked by Compose. Changing the value causes the entirety of thecontent
lambda where theCompositionLocal
is provided to be recomposed, instead of just the places where thecurrent
value is read in the Composition.
If the value provided to the CompositionLocal
is highly unlikely to change or
will never change, use staticCompositionLocalOf
to get performance benefits.
For example, an app's design system might be opinionated in the way composables
are elevated using a shadow for the UI component. Since the different
elevations for the app should propagate throughout the UI tree, we use a
CompositionLocal
. As the CompositionLocal
value is derived conditionally
based on the system theme, we use the compositionLocalOf
API:
// LocalElevations.kt file data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) // Define a CompositionLocal global object with a default // This instance can be accessed by all composables in the app val LocalElevations = compositionLocalOf { Elevations() }
Providing values to a CompositionLocal
The CompositionLocalProvider
composable binds values to CompositionLocal
instances for the given
hierarchy. To provide a new value to a CompositionLocal
, use the
provides
infix function that associates a CompositionLocal
key to a value
as follows:
// MyActivity.kt file class MyActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Calculate elevations based on the system theme val elevations = if (isSystemInDarkTheme()) { Elevations(card = 1.dp, default = 1.dp) } else { Elevations(card = 0.dp, default = 0.dp) } // Bind elevation as the value for LocalElevations CompositionLocalProvider(LocalElevations provides elevations) { // ... Content goes here ... // This part of Composition will see the `elevations` instance // when accessing LocalElevations.current } } } }
Consuming the CompositionLocal
CompositionLocal.current
returns the value provided by the nearest CompositionLocalProvider
that provides a value to that CompositionLocal
:
@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition MyCard(elevation = LocalElevations.current.card) { // Content } }
Alternatives to consider
A CompositionLocal
might be an excessive solution for some use cases. If your
use case doesn't meet the criteria specified in the Deciding whether to use
CompositionLocal section, another solution might likely be better
suited for your use case.
Pass explicit parameters
Being explicit about composable's dependencies is a good habit. We recommend that you pass composables only what they need. To encourage decoupling and reuse of composables, each composable should hold the least amount of information possible.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel.data) } // Don't pass the whole object! Just what the descendant needs. // Also, don't pass the ViewModel as an implicit dependency using // a CompositionLocal. @Composable fun MyDescendant(myViewModel: MyViewModel) { /* ... */ } // Pass only what the descendant needs @Composable fun MyDescendant(data: DataToDisplay) { // Display data }
Inversion of control
Another way to avoid passing unnecessary dependencies to a composable is via inversion of control. Instead of the descendant taking in a dependency to execute some logic, the parent does that instead.
See the following example where a descendant needs to trigger the request to load some data:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
Depending on the case, MyDescendant
might have a lot of responsibility. Also,
passing MyViewModel
as a dependency makes MyDescendant
less reusable since
they're now coupled together. Consider the alternative that doesn't pass the
dependency into the descendant and uses inversion of control principles that
makes the ancestor responsible for executing the logic:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
This approach can be better suited for some use cases as it decouples the child from its immediate ancestors. Ancestor composables tend to become more complex in favor of having more flexible lower-level composables.
Similarly, @Composable
content lambdas can be used in the same way to get
the same benefits:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Recommended for you
- Note: link text is displayed when JavaScript is off
- Anatomy of a theme in Compose
- Using Views in Compose
- Kotlin for Jetpack Compose