Passing data using CompositionLocal
March 7, 2022
CompositionLocal is an API in Compose that lets you pass data between Composables within tree hierarchy implicitly (without passing parameters).
TL;DR
Create a
CompositionLocalusingcompositionLocalOforstaticCompositionLocalOf(see below for difference)Give your
CompositionLocalobject a default value whenever possible or throw exception when uninitializedAccess current
CompositionLocalusing.currentLet UI tree access current
CompositionLocalas is or change it usingCompositionLocalProvider(no need to pass parameters to composables)Carefully consider alternatives, test recompositions
Let’s look at an example where movie info is passed in from a Movie Screen to a Movie Info screen. First let’s define our Movie CompositionLocal
data class Movie(val title: String, val year: Int)
val LocalMovie = compositionLocalOf<Movie> { error("Movie not set") }
Note the Local prefix—a naming convention to allow better discoverability with auto-complete in the IDE.
Initializing CompositionLocal
When initializing CompositionLocal, you want to either define a sensible default value or throw an exception when you try to access its value before it’s set. In our example, there is no reasonable default for a movie instance so we direct our CompositionLocal to throw an IllegalStateException if we try to access it before it’s set.
Here is the rest of our example:
data class Movie(val title: String, val year: Int)
val LocalMovie = compositionLocalOf<Movie> { error("Movie not set") }
Passing CompositionLocal
Once our CompositionLocal object has been created, we can retrieve it and update it using CompositionLocalProvider and pass it to lower nodes implicitly.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MovieScreen()
}
}
}
@Composable
private fun MovieScreen() {
val movie = Movie("The Batman", 2022)
CompositionLocalProvider(LocalMovie provides movie) {
MovieInfo()
}
// calling this outside of CompositionLocalProvider lambda
// will result in java.lang.IllegalStateException: Movie not set
MovieInfo()
}
@Composable
fun MovieInfo() {
Column {
Text("Movie title: " + LocalMovie.current.title)
Text("Year: " + LocalMovie.current.year)
}
}
A few things to note here:
Data is passed from
MovieScreentoMovieInfocomponent implicitly (without parameters) inside content lambda of theCompositionLocalProvider.Trying to render
MovieInfocomponent outside of theCompositionLocalProviderlambda will result inIllegalStateExceptionsince we did not initialize yet.If our
LocalMoviewas previously initialized, its value would be unchanged outside of theCompositionLocalProviderlambda.Note that the
LocalMovieis passed down the tree hierarchy—there is no side-effect here—the globalCompositionLocalvalue is unchanged when another composable accesses it.You access the movie details from
MovieInfousingLocalMovie.currentsyntax.
Alternatives to CompositionLocal
Note that in many cases, a great alternative to using CompositionLocal is passing state explicitly via parameters or ViewModel. Navigation graph-scoped ViewModel will often be a great choice as well. Check out https://developer.android.com/jetpack/compose/compositionlocal#deciding to see if CompositionLocal is right for your use case.
compositionLocalOf vs staticCompositionLocalOf
There are two APIs to create a CompositionLocal:
compositionLocalOf: Will invalidate only the content that reads its.currentvalue during recomposition.staticCompositionLocalOf: Changing the value causes the entirety of the content lambda where the CompositionLocal is provided to be recomposed, instead of just the places where the.currentvalue is read. For improved performance, usestaticCompositionLocalOfif the value in CompositionLocal is highly unlikely to ever change.
Examples of predefined CompositionLocals
LocalContext
Provides a Context that can be used by Android applications.
Show a Toast
val context = LocalContext.current Toast.makeText(context, R.string.mystring, Toast.LENGTH_SHORT).show()
Get Android resources
val context = LocalContext.current context.resources .openRawResource(R.raw.motion_scene) .readBytes() .decodeToString() }
Start new activity
val context = LocalContext.current
Button(
onClick = {
context.startActivity(MyActivity.newIntent(context))
}
)
LocalConfiguration
Android Configuration useful for determining how to organize the UI.
val configuration = LocalConfiguration.current
when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Text("Landscape")
}
else -> {
Text("Portrait")
}
}
LocalLifecycleOwner
CompositionLocal containing the current LifecycleOwner. For instance, you can make a view follow a lifecycle:
@Composable
private fun rememberMyViewLifecycleObserver(myView: View): LifecycleEventObserver =
remember(myView) {
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> myView.onCreate(Bundle())
Lifecycle.Event.ON_START -> myView.onStart()
Lifecycle.Event.ON_RESUME -> myView.onResume()
Lifecycle.Event.ON_PAUSE -> myView.onPause()
Lifecycle.Event.ON_STOP -> myView.onStop()
Lifecycle.Event.ON_DESTROY -> myView.onDestroy()
else -> throw IllegalStateException()
}
}
}
val lifecycleObserver = rememberMyViewLifecycleObserver(myView)
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}