Blog Infos
Author
Published
Topics
, , , ,
Published

Did you ever need to pass irrelevant parameters through multiple composable functions just to forward a parameter down the composable tree somewhere?

That’s exactly the problem Composition Locals solve! In this article, we’ll discover what Composition Locals are, why they’re so useful, and how to create your own.

What Are Composition Locals?

Composition Locals are like invisible messengers in Jetpack Compose. They allow any child composable — no matter how deeply nested — to read values provided higher up the composition tree. If you’ve used React Native, they’re similar to the Context API, which solves the “prop drilling” problem (passing props through components that don’t directly need them).

Example: Instead of passing a theme object to every composable, you can provide it once and have child composables fetch it as needed.

The Problem They Solve

Here’s a typical example without Composition Locals:

@Composable
fun MyApp(theme: Theme) {
    MainScreen(theme = theme)
}

@Composable
fun MainScreen(theme: Theme) {
    Column {
        Header(theme = theme)
        Content(theme = theme)
    }
}

@Composable
fun Header(theme: Theme) {
    // Use theme
}

@Composable
fun Content(theme: Theme) {
    // Use theme
}

Every composable has a theme parameter, even if it doesn’t use it. This leads to “parameter pollution” and makes the code harder to maintain.

Why Are Composition Locals Important?
  1. Reduced Boilerplate
    Provide data once at a higher level, consume it wherever necessary.
  2. Better Encapsulation
    Intermediate composables don’t need to manage data they don’t directly use.
  3. Easier Testing
    You can override Composition Locals in tests without changing public APIs.
  4. Scoped Values
    Data is only available to composables within the specific composition scope.
What Composition Locals Are (and Aren’t)

Due to a comment in LinkedIn about this post, let me make something clear…

CompositionLocals shine when you’re dealing with cross-cutting UI stuff like theming or spacing. That’s where they save you from passing the same parameters through every composable.

But for actual app logic its better to avoid using them.

Are For:
  • Design system tokens (themes, spacing, typography)
  • UI infrastructure (keyboard controller, focus management)
  • Cross-cutting UI concerns that follow the visual hierarchy
  • Values that need different overrides at different levels of your UI tree
Aren’t For:
  • Business logic or application state
  • Data that should be managed by state management solutions
  • Values that affect application behavior — Database connections, repositories, or other non-UI dependencies
  • Avoiding proper dependency injection

Think of Composition Locals as part of your UI toolkit, not as a general dependency injection mechanism. They shine when dealing with UI-related values that naturally follow your component hierarchy, but shouldn’t be used to bypass proper architecture patterns or hide important dependencies.

A good rule of thumb:

– if it’s about how things look or interact at the UI level, a CompositionLocal might be appropriate.

– If it’s about what your app does, use proper state management or dependency injection instead.

Creating and Using Composition Locals
1. Declare a Composition Local

 

// For dynamic values that change frequently
val LocalTheme = compositionLocalOf<Theme> { error("No theme provided!") }

// For values that rarely or never change at runtime
val LocalTheme = staticCompositionLocalOf { Theme.Light }

 

  • staticCompositionLocalOf: For values that never or rarely change during runtime (like design tokens—typography, spacing, etc.). This doesn’t automatically trigger recompositions if the value changes, making it more efficient.
  • compositionLocalOf: For dynamic values that change frequently (like a theme toggled by the user). When the provided value updates, any composable consuming it will automatically recompose.
2. Provide a Value

 

@Composable
fun MyApp() {
    val theme = Theme.Dark
    CompositionLocalProvider(LocalTheme provides theme) {
        MainScreen()
    }
}

 

3. Consume the Value

 

@Composable
fun MyComponent() {
    val theme = LocalTheme.current
    // Use theme
}

 

. . .

Comparing to React Native’s Context API

If you’re familiar with React Native’s Context API, Composition Locals feel very similar:

Jetpack Compose Example:

 

val LocalTheme = compositionLocalOf { Theme.Light }

@Composable
fun App() {
    CompositionLocalProvider(LocalTheme provides Theme.Dark) {
        ScreenA()
    }
}

@Composable
fun Header() {
    val theme = LocalTheme.current
    Text(
        text = "Title",
        color = if (theme == Theme.Dark) Color.White else Color.Black
    )
}

 

React Native Example:

 

// Create context
const ThemeContext = React.createContext(Theme.Light);

function App() {
  return (
    <ThemeContext.Provider value={Theme.Dark}>
      <MainScreen />
    </ThemeContext.Provider>
  );
}

function Header() {
  const theme = React.useContext(ThemeContext);
  return <h1 style={{ color: theme === Theme.Dark ? "#fff" : "#000" }}>Title</h1>;
}

 

 

Both approaches solve prop drilling, allow subtree overrides, and supply default values — but differ in syntax and recomposition mechanics.

Understanding Scope: Not Global by Default

A crucial point: Composition Locals are not automatically global! They’re scoped to wherever you provide them:

@Composable
fun App() {
    // LocalTheme is Dark only in ScreenA's hierarchy
    CompositionLocalProvider(LocalTheme provides Theme.Dark) {
        ScreenA()
    }
    
    // ScreenB uses default or throws an error if no default is set
    ScreenB()
    
    // LocalTheme is Light only in ScreenC's hierarchy
    CompositionLocalProvider(LocalTheme provides Theme.Light) {
        ScreenC()
    }
}

You can override values deeper down:

@Composable
fun ScreenA() {
    // Using Dark theme from parent
    Header()
    
    CompositionLocalProvider(LocalTheme provides Theme.Light) {
        // Overrides Dark theme
        SpecialSection()
    }

    // Back to Dark
    Footer()
}

. . .

 

Making Them “Global”

If you want values available throughout your entire app, provide them at the top level:

@Composable
fun MyApp() {
    CompositionLocalProvider(
        LocalTheme provides appTheme,
        LocalSpacing provides appSpacing
    ) {
        AppContent()
    }
}

That said, truly global state (like user profiles, auth tokens, or server data) often belongs in a dedicated state management solution (e.g., ViewModelHiltRedux, etc.).

Real-World Example: Spacing System

Let’s say we want a custom spacing system:

// 1. Define a spacing data class
data class Spacing(val small: Dp = 4.dp, val medium: Dp = 8.dp, val large: Dp = 16.dp)

// 2. Create a CompositionLocal
val LocalSpacing = compositionLocalOf { Spacing() }

// 3. Provide custom spacing for a specific screen
@Composable
fun SpecialScreen() {
    CompositionLocalProvider(LocalSpacing provides Spacing(8.dp, 16.dp, 24.dp)) {
        CustomCard("Hello", "This card uses custom spacing")
    }
}

// 4. Consume it
@Composable
fun CustomCard(title: String, content: String) {
    val spacing = LocalSpacing.current
    Card(modifier = Modifier.padding(spacing.medium)) {
        Column(modifier = Modifier.padding(spacing.medium)) {
            Text(title, modifier = Modifier.padding(bottom = spacing.small))
            Text(content)
        }
    }
}

. . .

@Preview Composables

An important gotcha: @Preview composables are completely isolated from your app’s composition tree. This means they can’t access any CompositionLocals that you’ve provided elsewhere in your app, even at the root level!

// In your app
@Composable
fun MyApp() {
    CompositionLocalProvider(LocalTheme provides Theme.Dark) {
        AppContent()  // All children here can access LocalTheme
    }
}

// In your preview - THIS WON'T WORK!
@Preview
@Composable
fun SomeComponentPreview() {
    MyComponent()  // CRASH! No access to LocalTheme
}

Instead, you need to provide all necessary CompositionLocals again in your preview:

@Preview
@Composable
fun SomeComponentPreview() {
    CompositionLocalProvider(LocalTheme provides Theme.Dark) {
        MyComponent()  // Now it works!
    }
}

To avoid repeating this setup in every preview, create a preview wrapper:

@Composable
private fun PreviewWrapper(
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalTheme provides Theme.Light,
        LocalSpacing provides Spacing(),
        LocalContentColor provides Color.Black,
        // Add other necessary CompositionLocals
    ) {
        Surface {  // Optional: provide a surface for proper theming
            content()
        }
    }
}

// Now you can easily create previews
@Preview
@Composable
fun ComponentPreview() {
    PreviewWrapper {
        MyComponent()
    }
}

This approach:

  • Makes previews more maintainable
  • Ensures consistency across previews
  • Reduces boilerplate
  • Makes it easy to add preview variants
Understanding Thread Usage with CompositionLocals
  • The .current property is synchronized with Compose’s composition system
  • You can’t accidentally access CompositionLocals from background threads because they’re only available in @Composable functions

 

// This is safe - always runs on the main thread during composition
@Composable
fun SafeComponent() {
    val theme = LocalTheme.current  // Thread-safe access
}

// This would crash - trying to access outside composition
CoroutineScope(Dispatchers.IO).launch {
    val theme = LocalTheme.current  // CRASH: Wrong thread and not in composition
}

 

2. Value Updates
  • Changes to CompositionLocal values are always synchronized with recomposition
  • Updates are atomic — you’ll never get partial or inconsistent values
  • All readers see the same value at the same point in the composition

 

@Composable
fun ThemeProvider(theme: Theme) {
    // Thread-safe: Updates are synchronized with composition
    CompositionLocalProvider(LocalTheme provides theme) {
        Content()
    }
}

 

3. Background Thread Considerations

If you need to use CompositionLocal values in background work:

@Composable
fun BackgroundWorker() {
    val theme = LocalTheme.current  // Safely capture during composition
    
    LaunchedEffect(theme) {
        withContext(Dispatchers.IO) {
            // Safe: using captured value, not accessing .current
            processTheme(theme)
        }
    }
}

 

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

No results found.

Usage Rules
What is .current?

The .current property is how you actually read a value from a CompositionLocal. Think of it as the “getter” that retrieves the most recently provided value in the composition tree above your composable.

// Defining a CompositionLocal
val LocalTheme = compositionLocalOf<Theme> { Theme.Light }

@Composable
fun MyComponent() {
    // Using .current to read the value
    val theme = LocalTheme.current  // This gets the closest provided Theme value
    
    Text(
        text = "Hello",
        color = theme.textColor  // Using the retrieved theme
    )
}
Built-in CompositionLocals

Jetpack Compose already comes with many useful Composition Locals. For instance:

  • LocalContext: Fetches the Android Context inside a composable.
  • LocalSoftwareKeyboardController: Helps you control the software keyboard (show/hide).
  • LocalFocusManager: Manages focus state across composables.

These are excellent examples of how Composition Locals make it easy to access shared resources without needing to pass them around everywhere.

Let’s see how to use .current with some built-in CompositionLocals:

@Composable
fun ExampleWithBuiltInLocals() {
    // Getting the Android Context
    val context = LocalContext.current
    
    // Getting the keyboard controller
    val keyboardController = LocalSoftwareKeyboardController.current
    
    // Getting the focus manager
    val focusManager = LocalFocusManager.current
    
    Button(onClick = {
        // Using the retrieved values
        Toast.makeText(context, "Clicked!", Toast.LENGTH_SHORT).show()
        keyboardController?.hide()  // Note: keyboard controller might be null
        focusManager.clearFocus()
    }) {
        Text("Click Me")
    }
}
1. Only Use .current in @Composable Functions

Composition Locals are tied to Compose’s composition system. The .current property can only be accessed during composition, which happens within @Composable functions. Trying to access it elsewhere will crash your app because the composition system isn’t active to provide the value.

// WRONG - Will crash!
class MyViewModel {
    private val theme = LocalTheme.current  // Crash: Not in a @Composable
}

// WRONG - Will crash!
fun normalFunction() {
    val theme = LocalTheme.current  // Crash: Not in a @Composable
}
// RIGHT
@Composable
fun MyComponent() {
    val theme = LocalTheme.current  // OK: Inside a @Composable
}
2. Don’t Store .current Values

CompositionLocal values can change during recomposition. If you store a .current value in a property or long-lived variable, that stored value might become stale when the actual CompositionLocal value changes. Always read the value fresh when you need it.

// WRONG
@Composable
fun BadExample() {
    // Don't store in properties or variables that outlive the composition
    class StateHolder {
        val theme = LocalTheme.current  // BAD: Value might become stale
    }
}

// RIGHT
@Composable
fun GoodExample() {
    // Read the value each time you need it
    val theme = LocalTheme.current
    
    Button(
        onClick = { /* ... */ },
        colors = ButtonDefaults.buttonColors(
            backgroundColor = theme.buttonColor  // Fresh value each recomposition
        )
    ) {
        Text("Click Me")
    }
}
3. Passing to Non-Composable Code

Sometimes you need to use CompositionLocal values in regular classes or functions. The safe way to do this is to read the value in your composable and pass it as a parameter. This ensures the non-composable code always has the current value and maintains proper separation of concerns.

class ThemeAwareHelper(private val theme: Theme) {
    fun getStyledText(text: String): SpannedString {
        // Use theme safely here
    }
}

@Composable
fun MyComponent() {
    val theme = LocalTheme.current
    val helper = remember(theme) { ThemeAwareHelper(theme) }
    
    // Use helper safely
}
4. Accessing in Side Effects

When using CompositionLocal values in side effects (like LaunchedEffect or SideEffect), capture the value during composition and use it as a parameter to ensure the side effect reruns when the value changes.

@Composable
fun MyComponent() {
    val theme = LocalTheme.current
    
    // Side effect will relaunch if theme changes
    LaunchedEffect(theme) {
        // Safe to use theme here as it's captured from composition
        analytics.logThemeUsed(theme.name)
    }
    
    // DisposableEffect example
    DisposableEffect(theme) {
        val themeListener = ThemeListener(theme)
        themeListener.start()
        
        onDispose {
            themeListener.stop()
        }
    }
}
5. Multiple CompositionLocals

When your composable needs multiple CompositionLocal values, read each one individually at the top level of your composable. This makes it clear what values your composable depends on and ensures you’re always working with fresh values.

@Composable
fun MyComponent() {
    // Read all needed values at the start
    val theme = LocalTheme.current
    val spacing = LocalSpacing.current
    val context = LocalContext.current
    
    Column(
        modifier = Modifier.padding(spacing.medium)
    ) {
        Text(
            text = "Hello",
            style = theme.typography.body1,
            color = theme.textColor
        )
        
        Button(onClick = {
            // Using context safely
            Toast.makeText(context, "Clicked!", Toast.LENGTH_SHORT).show()
        }) {
            Text("Click Me")
        }
    }
}

These rules ensure that:

  • Your app doesn’t crash from incorrect CompositionLocal access
  • Values stay up-to-date when CompositionLocals change
  • Side effects and non-composable code work correctly with CompositionLocal values
  • Your code remains maintainable and predictable
  • Recomposition works properly with your CompositionLocal usage
Practical Examples with MVI/MVVM

Even if you’re using MVI (Model-View-Intent) or MVVM, Composition Locals remain helpful for UI infrastructure or cross-cutting concerns — like theming, feature flags, or navigation — while your state management handles the business logic and data flow. Here are some real-world scenarios:

  • Design System
    Use staticCompositionLocalOf for values such as colors, typography, and spacing. These design tokens rarely change at runtime, so a static local avoids unnecessary recompositions.
  • Snackbar Manager
    Make a shared SnackbarManager accessible via a Composition Local. Any screen can trigger a global snackbar message without passing extra parameters or polluting the ViewModel.
  • Feature Flags / A/B Testing
    Store feature flags in a Composition Local so your UI can easily reflect whether a feature is enabled or disabled. This prevents flag data from cluttering your application’s primary state.
  • Accessibility Settings
    Provide user accessibility preferences (font scale, reduce motion, high contrast, etc.) through a Composition Local, allowing any composable to adapt UI based on these settings.

By leveraging Composition Locals for these cross-cutting concerns, your MVI/MVVM architecture remains focused on state and logic, while the UI benefits from easier access to shared resources.

Best Practices
  1. Choose the Right Type
    Use compositionLocalOf for values that change and staticCompositionLocalOf for values that rarely change.
  2. Provide Default Values
    Always give a sensible default or throw an error when creating your CompositionLocal.
  3. Limit Scope
    Avoid providing everything globally. Keep values in the smallest scope needed.
  4. Document Usage
    Help other developers (and future you!) understand when and how to use your Composition Locals.
  5. Consider Performance
    Changes in compositionLocalOf cause recomposition, while staticCompositionLocalOf does not.
Conclusion

Composition Locals are a powerful way to share data in Jetpack Compose. They shine when you have UI-related values (theme, spacing, navigation, etc.) that need to be accessible throughout your composables — without the hassle of passing parameters everywhere.

However, remember that they’re not a replacement for proper state management. Keep your app-specific or user interaction data in your MVI/MVVM state, and let Composition Locals handle cross-cutting concerns and design tokens.

By using Composition Locals effectively alongside a robust architecture, you’ll write cleaner, more scalable, and easier-to-test Compose code.

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu