What You'll Learn
This article explains CompositionLocal (staticCompositionLocalOf, compositionLocalOf, custom Provider, theme integration, testing).
What is CompositionLocal
A mechanism to implicitly pass data between Composables, avoiding explicit parameter threading.
// Built-in CompositionLocal examples
@Composable
fun Example() {
val context = LocalContext.current // Context
val density = LocalDensity.current // Density
val config = LocalConfiguration.current // Configuration
val lifecycle = LocalLifecycleOwner.current // LifecycleOwner
}
Custom CompositionLocal
// staticCompositionLocalOf: for values that rarely change (wider recomposition)
val LocalAppConfig = staticCompositionLocalOf<AppConfig> {
error("No AppConfig provided")
}
// compositionLocalOf: for values that change frequently (narrow recomposition)
val LocalUserSession = compositionLocalOf<UserSession?> { null }
data class AppConfig(
val apiBaseUrl: String,
val isDebug: Boolean,
val appVersion: String
)
data class UserSession(
val userId: String,
val displayName: String,
val isAdmin: Boolean
)
Provider
@Composable
fun App() {
val appConfig = AppConfig(
apiBaseUrl = BuildConfig.API_URL,
isDebug = BuildConfig.DEBUG,
appVersion = BuildConfig.VERSION_NAME
)
val userSession = remember { mutableStateOf<UserSession?>(null) }
CompositionLocalProvider(
LocalAppConfig provides appConfig,
LocalUserSession provides userSession.value
) {
AppNavigation()
}
}
// Access in child Composables
@Composable
fun SettingsScreen() {
val config = LocalAppConfig.current
val session = LocalUserSession.current
Column(Modifier.padding(16.dp)) {
Text("Version: ${config.appVersion}")
session?.let { Text("User: ${it.displayName}") }
}
}
Custom Theme Values
@Immutable
data class AppSpacing(
val small: Dp = 4.dp,
val medium: Dp = 8.dp,
val large: Dp = 16.dp,
val extraLarge: Dp = 24.dp
)
val LocalSpacing = staticCompositionLocalOf { AppSpacing() }
// Use as MaterialTheme extension
@Composable
fun AppTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalSpacing provides AppSpacing()) {
MaterialTheme(content = content)
}
}
// Accessor extension property
val MaterialTheme.spacing: AppSpacing
@Composable @ReadOnlyComposable
get() = LocalSpacing.current
// Usage
@Composable
fun MyScreen() {
Column(Modifier.padding(MaterialTheme.spacing.large)) {
Text("Hello", Modifier.padding(bottom = MaterialTheme.spacing.medium))
}
}
Testing
@Test
fun testWithCustomCompositionLocal() {
val testConfig = AppConfig(
apiBaseUrl = "https://test.api.com",
isDebug = true,
appVersion = "1.0.0-test"
)
composeTestRule.setContent {
CompositionLocalProvider(LocalAppConfig provides testConfig) {
SettingsScreen()
}
}
composeTestRule.onNodeWithText("Version: 1.0.0-test").assertIsDisplayed()
}
Summary
| API | Use Case |
|---|---|
compositionLocalOf |
Frequently changing values |
staticCompositionLocalOf |
Rarely changing values |
CompositionLocalProvider |
Provide values |
.current |
Access values |
-
staticCompositionLocalOfrecomposes entire subtree when changed -
compositionLocalOfrecomposes only reading locations - Perfect for theme extensions (Spacing, Elevation, etc.)
- Use
CompositionLocalProviderfor testing
8 production-ready Android app templates (custom theme support) are available.
Browse templates → Gumroad
Related articles:
- Material3 theming
- Dark mode
- Custom Composables
Ready-Made Android App Templates
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
Top comments (0)