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
CompositionLocal
usingcompositionLocalOf
orstaticCompositionLocalOf
(see below for difference)Give your
CompositionLocal
object a default value whenever possible or throw exception when uninitializedAccess current
CompositionLocal
using.current
Let UI tree access current
CompositionLocal
as 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
MovieScreen
toMovieInfo
component implicitly (without parameters) inside content lambda of theCompositionLocalProvider
.Trying to render
MovieInfo
component outside of theCompositionLocalProvider
lambda will result inIllegalStateException
since we did not initialize yet.If our
LocalMovie
was previously initialized, its value would be unchanged outside of theCompositionLocalProvider
lambda.Note that the
LocalMovie
is passed down the tree hierarchy—there is no side-effect here—the globalCompositionLocal
value is unchanged when another composable accesses it.You access the movie details from
MovieInfo
usingLocalMovie.current
syntax.
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.current
value 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.current
value is read. For improved performance, usestaticCompositionLocalOf
if 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) } }