CompositionLocal

Jetpack Compose Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Pick the fix

A team passes the screen's ViewModel through a CompositionLocal to avoid parameters. What's the best guidance?

Options
Hint 1 of 1

What does a preview of a child need to provide?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Pass state and lambdas as parameters; keep CompositionLocal for tree-wide values like theme

Correct. Explicit parameters keep dependencies visible and previews simple.

Why not the others?
  • B Switch to staticCompositionLocalOf. That changes recomposition, not the design problem.
  • C Make the ViewModel a global object. That's worse: shared, untestable state.
  • D It's the recommended pattern. Compose guidance warns against hiding dependencies this way.

30-second answer

A CompositionLocal passes a value implicitly down the tree, so composables deep inside can read it without every level passing it as a parameter. The theme, LocalContext and LocalLifecycleOwner work this way. It's the wrong tool for ordinary screen data or dependencies, because it hides what a composable needs and makes previews and tests surprising.

Practise saying it · 30 s. No recording, just a timer

Explain CompositionLocal and its limits.

Say your answer out loud, then check which of these points you covered:

  • Implicit values down the tree
  • Theme and context are examples
  • Not for screen data or ViewModels
  • static vs dynamic recomposition

Deep dive

compositionLocalOf recomposes only readers when the value changes; staticCompositionLocalOf recomposes the whole subtree but is cheaper when it rarely changes.

Provide values with CompositionLocalProvider(LocalX provides value) { }.

Good fits: theming, spacing, analytics loggers that are truly global.

Give it a sensible default, or throw in the default factory so a missing provider is obvious.

A spacing local Compile-checked · Kotlin 2.4.21
data class Spacing(val small: Dp = 4.dp, val medium: Dp = 8.dp)

val LocalSpacing = staticCompositionLocalOf { Spacing() }

@Composable
fun SpacedCard(content: @Composable () -> Unit) {
    Box(Modifier.padding(LocalSpacing.current.medium)) { content() }
}

Common mistakes

  • Passing a ViewModel through a CompositionLocal.
  • Using it to avoid a few parameters.
  • Choosing staticCompositionLocalOf for a value that changes often.

Senior take & follow-ups

Show judgement: name good and bad uses. Interviewers often follow up with 'would you put the ViewModel in one?'

Follow-up questions

Think of your answer first, then open the hint.

  1. Why is the theme a CompositionLocal?

    Show hint

    Almost every composable needs it, and passing it everywhere would be noise.

  2. staticCompositionLocalOf vs compositionLocalOf?

    Show hint

    Static: whole subtree recomposes on change; dynamic: only readers.

Compare on iOS: Custom environment values with @Entry SwiftUI's Environment plays the same role.

Suggest a fix if something here is wrong or out of date.