Jetpack Compose gives you CompositionLocals to pass data implicitly down your UI tree. You define them with either compositionLocalOf or staticCompositionLocalOf. Choosing the right one can make your app more responsive—and faster.

What’s Going On Under the Hood?

compositionLocalOf

  • Wraps your value in Compose’s snapshot-state system
  • Reads become state reads
  • Any composable reading the value will recompose when it changes

staticCompositionLocalOf

  • Just slots the value into the Composition tree—no state wrapping
  • No automatic recomposition when value changes
  • Lighter weight, better performance

When to Use Which

ScenarioUse This API
Theme changes (dark/light)compositionLocalOf
Dynamic user settings (font size, locale)compositionLocalOf
Navigation or ViewModel accesscompositionLocalOf
Layout constants (spacing, padding)staticCompositionLocalOf
Immutable assets (typefaces, colors)staticCompositionLocalOf
Singletons (analytics, logging services)staticCompositionLocalOf

Example 1: Theme Switching (use compositionLocalOf)

// 1) Define a CompositionLocal for theme colors
val LocalColors = compositionLocalOf {
    lightColorPalette()   // default value
}
 
@Composable
fun MyApp(themeDark: Boolean, content: @Composable () -> Unit) {
    val palette = if (themeDark) darkColorPalette() else lightColorPalette()
    CompositionLocalProvider(LocalColors provides palette) {
        content()
    }
}
 
@Composable
fun ThemedBox() {
    // Reads LocalColors.current → state read
    val colors = LocalColors.current
    Box(
        Modifier
            .size(100.dp)
            .background(colors.primary)
    )
}

When themeDark flips, MyApp provides a new palette. Anything reading LocalColors.current recomposes automatically.

Example 2: Spacing Values (use staticCompositionLocalOf)

// 1) Define a static local for spacing
data class Spacing(val small: Dp, val medium: Dp, val large: Dp)
 
val LocalSpacing = staticCompositionLocalOf {
    Spacing(4.dp, 8.dp, 16.dp)  // default constants
}
 
@Composable
fun AppScaffold(content: @Composable () -> Unit) {
    // You could override spacing once at the root…
    CompositionLocalProvider(LocalSpacing provides Spacing(6.dp, 12.dp, 24.dp)) {
        content()
    }
}
 
@Composable
fun PaddedColumn() {
    val space = LocalSpacing.current
    Column(Modifier.padding(space.medium)) {
        Text("Item 1")
        Spacer(Modifier.height(space.small))
        Text("Item 2")
    }
}

Even if you re-provide LocalSpacing with a new Spacing(...), PaddedColumn won’t recompose just because spacing changed—only other state-driven events can force it.

Performance Takeaways

  1. staticCompositionLocalOf is lighter

    • No snapshot-state overhead
    • Better memory and CPU footprint
  2. Don’t wrap constant objects in compositionLocalOf

    • You’ll pay for needless recomposition tracking
  3. Don’t expect automatic updates from staticCompositionLocalOf

    • Changes won’t trigger recomposition

Quick Rule of Thumb

  • Need automatic, immediate updates? → compositionLocalOf
  • Static value or updates don’t matter? → staticCompositionLocalOf

By picking the right CompositionLocal API, you keep your Compose UI both reactive where it matters and efficient where it doesn’t.

To learn more about Jetpack Compose and state management: