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
Scenario | Use This API |
---|---|
Theme changes (dark/light) | compositionLocalOf |
Dynamic user settings (font size, locale) | compositionLocalOf |
Navigation or ViewModel access | compositionLocalOf |
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
-
staticCompositionLocalOf is lighter
- No snapshot-state overhead
- Better memory and CPU footprint
-
Don’t wrap constant objects in compositionLocalOf
- You’ll pay for needless recomposition tracking
-
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.
Related Articles
To learn more about Jetpack Compose and state management:
- Jetpack Compose: Architecture - Understand the layered architecture of Compose
- Jetpack Compose: Composition Phase - Learn about UI rendering and state management
- UI Layer with UDF Pattern - Implementing clean architecture with Compose
- Kotlin Coroutines: Dispatchers - Managing background tasks in your Compose app