CompositionLocal is a powerful mechanism in Jetpack Compose that allows you to propagate values down the composition tree without having to pass them explicitly through every composable. This technique is similar to dependency injection and enables your UI to be more modular and maintainable.
In this blog post, we will discuss:
What CompositionLocal is
How to use CompositionLocal
When to use CompositionLocal
Creating custom CompositionLocals
How providers work in CompositionLocal
We'll also illustrate these concepts with real code examples.
What is CompositionLocal?
In traditional Android UI development, you might use context or dependency injection to share data across components. In Jetpack Compose, CompositionLocal provides a way to implicitly pass data down the UI tree. This data can include theme settings, configurations, or any state that should be shared among multiple composables.
CompositionLocal is especially useful when you have values that many composables depend on but that don’t change frequently. Instead of explicitly passing these values as parameters to every composable, you can define a CompositionLocal that “remembers” these values and makes them available throughout the UI hierarchy.
How to Use CompositionLocal
Using CompositionLocal involves two main steps:
Defining the CompositionLocal:
You define a CompositionLocal object with a default value. This can be done using thecompositionLocalOf
orstaticCompositionLocalOf
functions. The static version is optimized for values that do not change often.Providing a Value:
You use theCompositionLocalProvider
composable to override the default value with a specific one in a given part of the UI hierarchy.Consuming the Value:
Within any composable that is a descendant of the provider, you can access the value via thecurrent
property of your CompositionLocal.
Basic Example
Below is a simple example that defines a CompositionLocal for a theme color and then consumes it:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.material.Text
import androidx.compose.ui.graphics.Color
// 1. Define a CompositionLocal with a default color
val LocalThemeColor = compositionLocalOf { Color.Black }
@Composable
fun AppContent() {
// 2. Provide a custom theme color
CompositionLocalProvider(LocalThemeColor provides Color.Blue) {
ThemedText()
}
}
@Composable
fun ThemedText() {
// 3. Consume the CompositionLocal value
val themeColor = LocalThemeColor.current
Text("Hello, Compose!", color = themeColor)
}
In this example:
Definition: We create
LocalThemeColor
with a default ofColor.Black
.Provision: In
AppContent
, we provideColor.Blue
to the subtree.Consumption: In
ThemedText
, we access the current color and apply it to the text.
When to Use CompositionLocal
CompositionLocal is best suited for scenarios where you have values that need to be accessible by many composables but aren’t part of the explicit parameters. Common use cases include:
Theming: Passing down colors, typography, or shapes.
Localization: Providing locale or formatting information.
Configuration: Supplying configuration data like layout directions or accessibility settings.
Dependency Injection: Sharing a repository, network client, or any other dependency across composables.
Because CompositionLocal provides implicit access, it can simplify your code and make it cleaner. However, it should be used judiciously to avoid “hidden” dependencies that make your code harder to understand and test.
Creating Custom CompositionLocals
While the Android framework and Jetpack Compose provide a number of built-in CompositionLocals (like those for theming), you can also create your own. The process is straightforward:
Define your custom CompositionLocal:
Use eithercompositionLocalOf
orstaticCompositionLocalOf
to define your custom local value.Wrap the relevant portion of your UI in a CompositionLocalProvider:
Use the provider to set the value for your custom CompositionLocal.Consume the custom value in your composables.
Custom CompositionLocal Example
Let’s create a custom CompositionLocal that holds user configuration data:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
data class UserSettings(val darkModeEnabled: Boolean, val fontSize: Int)
// 1. Define the CompositionLocal with a default UserSettings instance
val LocalUserSettings = compositionLocalOf { UserSettings(darkModeEnabled = false, fontSize = 14) }
@Composable
fun SettingsScreen() {
// 2. Provide custom settings for the subtree
val customSettings = UserSettings(darkModeEnabled = true, fontSize = 18)
CompositionLocalProvider(LocalUserSettings provides customSettings) {
SettingsText()
}
}
@Composable
fun SettingsText() {
// 3. Consume the CompositionLocal value
val settings = LocalUserSettings.current
val mode = if (settings.darkModeEnabled) "Dark Mode" else "Light Mode"
Text("Mode: $mode, Font Size: ${settings.fontSize}")
}
In this code:
Definition: We define
LocalUserSettings
with a default configuration.Provision: We supply a custom user setting inside
SettingsScreen
.Consumption:
SettingsText
retrieves and displays the settings.
Advanced Example: Shared Transition and Animated Visibility Scopes
In more complex UI scenarios, you might want to share behavior or state across a navigation flow or animated transitions. Consider the following example that sets up two custom CompositionLocals—one for a SharedTransitionScope
and another for an AnimatedVisibilityScope
.
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
// Define the CompositionLocals with default values of null.
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
@Composable
fun TransitionProviderExample() {
// Create instances of the scopes.
val sharedTransitionScope = SharedTransitionScope()
val animatedVisibilityScope = AnimatedVisibilityScope()
// Provide the scope instances to the composition.
CompositionLocalProvider(
LocalSharedTransitionScope provides sharedTransitionScope,
LocalNavAnimatedVisibilityScope provides animatedVisibilityScope
) {
// Inside this block, the provided scopes can be consumed by child composables.
ChildComposable()
}
}
@Composable
fun ChildComposable() {
// Retrieve the current scope instances from the CompositionLocals.
val transitionScope = LocalSharedTransitionScope.current
val visibilityScope = LocalNavAnimatedVisibilityScope.current
// Use the scopes to perform actions or configure UI behavior.
// Here we display simple text indicating whether the scopes are available.
Text("SharedTransitionScope available: ${transitionScope != null}")
Text("AnimatedVisibilityScope available: ${visibilityScope != null}")
// Optionally, invoke functions on these scopes.
transitionScope?.doTransition()
visibilityScope?.animateVisibility()
}
How This Advanced Example Works
Scope Definitions:
SharedTransitionScope
andAnimatedVisibilityScope
, simulate shared functionality such as transitions and animations.CompositionLocal Definitions:
We define two CompositionLocals with default values ofnull
:
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null } val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
Providing Values:
In theTransitionProviderExample
composable, we instantiate both scopes and provide them to the composition usingCompositionLocalProvider
.Consuming Values:
InChildComposable
, the provided scopes are accessed using the.current
property. We then display text to indicate their availability and invoke their functions.
This pattern is very useful when you need to share behavior across a complex UI without cluttering your function parameters.
How Providers Work in CompositionLocal
The provider in CompositionLocal is implemented through the CompositionLocalProvider
composable. It works by overriding the default value of a CompositionLocal for its child composables. This is analogous to how React’s Context providers work.
Key Points:
Scope: The value provided is only accessible to the composables that are within the provider’s scope. If a composable lies outside this scope, it will fall back to the default value.
Recomposition: When the value provided changes, only the composables that read the value will be recomposed, ensuring efficient updates.
Stacking Providers: You can nest multiple providers to override values at different levels in your UI tree.
Conclusion
CompositionLocal is an essential tool in Jetpack Compose for managing implicit state and configuration across your UI. By defining a CompositionLocal, providing a value with CompositionLocalProvider
, and consuming it in your composables, you can create cleaner, more modular, and easier-to-manage UI code.
Whether you’re handling themes, localization, or more advanced use cases like shared transitions and animated visibility, CompositionLocal offers a flexible and efficient approach. Use it thoughtfully to keep implicit dependencies clear and maintainable.
Happy composing!
Want More Such Content?
Join my Substack community!
I'm continually sharing practical tutorials, tips, and insights from my Android development journey. Plus, we've launched a referral leaderboard to rewards our most active subscribers. Invite your friends, colleagues, and let's learn and grow together!
Join our WhatsApp Channel to get regular updates
Try implementing this yourself, share your results, tag me, and let's spread the magic of Jetpack Compose together!
Happy coding! 🎉