Question #389HardNative Android

Explain CompositionLocal and its use cases in Jetpack Compose?

#jetpack-compose#composition-local#dependency-injection

Answer

Overview

CompositionLocal provides implicit dependency injection down the composition tree.


Problem It Solves

kotlin
// ❌ Prop drilling - passing through many layers
@Composable
fun App(theme: Theme) {
    Screen(theme)
}

@Composable
fun Screen(theme: Theme) {
    Content(theme)
}

@Composable
fun Content(theme: Theme) {
    Text("Hello", color = theme.textColor)
}

Solution with CompositionLocal

kotlin
// Define CompositionLocal
val LocalTheme = compositionLocalOf<Theme> {
    error("No theme provided")
}

// Provide value
@Composable
fun App() {
    CompositionLocalProvider(LocalTheme provides DarkTheme) {
        Screen() // No need to pass theme
    }
}

// Consume value
@Composable
fun Content() {
    val theme = LocalTheme.current
    Text("Hello", color = theme.textColor)
}

Built-in CompositionLocals

kotlin
// Context
val context = LocalContext.current

// Configuration
val configuration = LocalConfiguration.current

// Density
val density = LocalDensity.current

// Text style
val textStyle = LocalTextStyle.current

Creating Custom CompositionLocal

Static Value

kotlin
data class AppConfig(val apiKey: String, val baseUrl: String)

val LocalAppConfig = staticCompositionLocalOf {
    AppConfig("", "")
}

Dynamic Value

kotlin
val LocalUser = compositionLocalOf<User?> { null }

@Composable
fun UserProvider(user: User, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUser provides user) {
        content()
    }
}

Use Cases

1. Theme

kotlin
@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalColors provides colors,
        LocalTypography provides typography
    ) {
        content()
    }
}

2. Navigation

kotlin
val LocalNavController = compositionLocalOf<NavController> {
    error("No NavController")
}

@Composable
fun MyButton() {
    val navController = LocalNavController.current
    Button(onClick = { navController.navigate("details") }) {
        Text("Go")
    }
}

3. Repository/ViewModel

kotlin
val LocalRepository = compositionLocalOf<UserRepository> {
    error("No repository")
}

@Composable
fun App() {
    val repository = remember { UserRepository() }
    
    CompositionLocalProvider(LocalRepository provides repository) {
        HomeScreen()
    }
}

staticCompositionLocalOf vs compositionLocalOf

kotlin
// Use staticCompositionLocalOf when value rarely changes
val LocalAppConfig = staticCompositionLocalOf { AppConfig() }

// Use compositionLocalOf when value changes frequently
val LocalUser = compositionLocalOf<User?> { null }

Performance:

  • text
    staticCompositionLocalOf
    : Changing value recomposes entire tree
  • text
    compositionLocalOf
    : Only recomposes consumers

Best Practice: Use CompositionLocal sparingly. Explicit parameters are usually better.