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 using compositionLocalOf or staticCompositionLocalOf (see below for difference)

  • Give your CompositionLocal object a default value whenever possible or throw exception when uninitialized

  • Access current CompositionLocal using .current

  • Let UI tree access current CompositionLocal as is or change it using CompositionLocalProvider (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:

  1. Data is passed from MovieScreen to MovieInfo component implicitly (without parameters) inside content lambda of the CompositionLocalProvider.

  2. Trying to render MovieInfo component outside of the CompositionLocalProvider lambda will result in IllegalStateException since we did not initialize yet.

    If our LocalMovie was previously initialized, its value would be unchanged outside of the CompositionLocalProvider lambda.

  3. Note that the LocalMovie is passed down the tree hierarchy—there is no side-effect here—the global CompositionLocal value is unchanged when another composable accesses it.

  4. You access the movie details from MovieInfo using LocalMovie.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, use staticCompositionLocalOf 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)
    }
}
 
Previous
Previous

Compose Row, Column and Scoped Modifiers

Next
Next

Compose remember vs remember mutableStateOf