When building Android apps with Jetpack Compose, you’ll often need to share data across your UI tree. Passing parameters down every Composable quickly becomes messy. That’s where CompositionLocal comes in.
Think of CompositionLocal as a smart way to provide values (like theme, locale, or user preferences) to multiple Composables without having to manually thread them through function parameters. It’s like dependency injection — but scoped to the Compose world.
In this post, we’ll explore how CompositionLocal works, why it matters for building scalable UI, and how you can use it effectively.
What is CompositionLocal?
CompositionLocal is a mechanism that allows you to define and access values that are automatically propagated down the Composable hierarchy.
- It provides contextual values (like theme colors or configurations).
- It removes the need to pass arguments everywhere.
- It helps you scale UI architecture by keeping components decoupled.
Jetpack Compose already uses CompositionLocal under the hood for things like MaterialTheme
, text styles, and layout direction.
Defining a CompositionLocal
You start by creating a CompositionLocal with a default value:
val LocalUser = compositionLocalOf<String> {
error("No user provided")
}
Here:
compositionLocalOf
creates a CompositionLocal with a default (or error if missing).- We’re saying: “If no user is provided, throw an error.”
Providing a Value
To inject a value, you use CompositionLocalProvider
:
@Composable
fun AppContent() {
CompositionLocalProvider(LocalUser provides "amol pawar") {
UserProfile()
}
}
Inside AppContent
, any child Composable can access LocalUser
.
Consuming a CompositionLocal
To read the value, use .current
:
@Composable
fun Dashboard() {
Column {
CompositionLocalProvider(LocalUser provides "akshay") {
UserProfile() // shows "Hello, askhay!"
}
UserProfile() // shows "Hello, amol pawar!"
}
}
Output:
Hello, amol pawar!
No need to pass user
down as a parameter—CompositionLocal handles it.
Why Use CompositionLocal?
Let’s break it down with a practical example. Imagine a large app with:
- Theme data (colors, typography).
- User session info.
- App settings like dark mode, locale, etc.
Passing these manually would be a nightmare. With CompositionLocal, you define them once and let the UI tree consume them where needed.
Scoped Values for Flexibility
One powerful feature is scoping. You can override a CompositionLocal in a subtree without affecting the rest of the app.
@Composable
fun Dashboard() {
Column {
CompositionLocalProvider(LocalUser provides "akshay") {
UserProfile() // shows "Hello, askhay!"
}
UserProfile() // shows "Hello, amol pawar!"
}
}
The value depends on where the Composable is in the hierarchy. This makes it perfect for context-specific overrides (like previewing different themes).
Best Practices for CompositionLocal
- Don’t abuse it. Use CompositionLocal for global or contextual data, not just to avoid passing parameters.
- Keep defaults meaningful. Provide safe defaults or throw an error if the value is critical.
- Use for ambient context. Theme, locale, user, system settings — these are ideal use cases.
- Avoid hidden dependencies. If a Composable always needs a value, prefer explicit parameters for clarity.
Theme System with CompositionLocal
Let’s create a mini theme system:
data class MyColors(val primary: Color, val background: Color)
val LocalColors = staticCompositionLocalOf<MyColors> {
error("No colors provided")
}
@Composable
fun MyTheme(content: @Composable () -> Unit) {
val colors = MyColors(primary = Color.Blue, background = Color.White)
CompositionLocalProvider(LocalColors provides colors) {
content()
}
}
@Composable
fun ThemedButton() {
val colors = LocalColors.current
Button(onClick = {}) {
Text("Click Me", color = colors.primary)
}
}
Usage:
@Composable
fun App() {
MyTheme {
ThemedButton()
}
Here, ThemedButton
gets its styling from LocalColors
without needing parameters.
CompositionLocal vs Parameters
- Use parameters when data is essential to the Composable.
- Use CompositionLocal when data is contextual, like theming or configuration.
This balance keeps your UI scalable and maintainable.
Conclusion
CompositionLocal is one of the most powerful tools in Jetpack Compose for writing scalable UI. It keeps your code cleaner, reduces boilerplate, and makes context handling a breeze.
By using CompositionLocal wisely, you can:
- Share contextual data easily
- Override values locally
- Keep UI components decoupled and reusable
Next time you’re passing a value through five different Composables, stop and ask yourself — could CompositionLocal handle this better?