Local Composition Provider in Jetpack Compose is considered a game-changer for managing composables and their dependencies, making your UI code more organized and maintainable.
With Local Composition Provider, you can effortlessly pass dependencies down the composition hierarchy, ensuring that each composable has access to the data it needs without excessive boilerplate. It promotes cleaner and more modular code, making your UI development a breeze.
So before we start, let's understand what’s the UI Tree.
It's a fundamental concept in Compose, the UI Tree refers to the hierarchical structure of composables that make up the user interface of an Android app.
In simple words, Composables are organized in a hierarchical structure, starting with a parent node and each node has one or more composables nested inside others. This hierarchy determines the layout and visual appearance of the UI.
Now, let's move to what’s the CompositionLocal.
Composition Local is a tool for passing data & objects down through the UI tree implicitly, which provides an easier and more organized way to use the shared data between multiple screens without the need to pass it around as Composables parameters which can be hard to manage for the complex and large apps.
An example of this mechanism is the colors, shapes, and typography objects that are provided by the MaterialTheme of your Compose project which you can access throughout your UI Tree.
@Composable
fun MyAppRootComposable() {
MyAppTheme {
...
}
}
// Somewhere in the project's UI Tree Hierarachy
@Composable
fun UserInfoCard(user: User) {
Row(
// `background` is obtained from MaterialTheme's LocalColors CompositionLocal.
modifier = Modifier
.background(MaterialTheme.colorScheme.background),
) {
...
}
}
So, What does make it efficient?
A CompositionLocal instance is scoped to a specific part of the Composition which allows you to provide different values at different levels of the tree. The current value of a CompositionLocal corresponds to the closest value provided by an ancestor in that part of the Composition.
For example, you can use the built-in LocalLayoutDirection LocalComposition for supporting the LTR and RTL within your app, but still be easily able to keep some UI parts in a specific direction, as is the case with OTP input in every app you build!
@Composable
fun OTPVerificationScreen() {
// By default, your app will follow system language, and provide the proper LayoutDirection for it
Column(
modifier = Modifier
.fillMaxSize(),
) {
...
// Only your OTP input will have Ltr direction no matter what the current langauge is!
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
// Your OTP input UI code
}
}
}
When to use the CompositionLocal?
Glad you asked, deciding on whether to use CompositionLocal or not depends on two main conditions, which are :
The compositionLocal SHOULD have a good default value to start with, in case there is no default value you should ensure that it’s completely difficult to have a situation where the LocalComposition’s value is not provided which may cause a lot of problems and unpredictable behavior when creating tests or trying to preview a descendant Composable that use that CompositionLocal value.
You should avoid CompositionLocal in case only a few descendants need that value, and use explicit parameters for that few descendant Composables.
Types and definitions of the CompositionLocal
After analyzing your requirements and use cases and deciding to use CompositionLocal, you should know that two types of CompositionLocal can be used, and each has its unique functionality.
The CompositionLocal objects are always defined as top-level so that they can be accessed from anywhere in your project, which is the main point of using it in the first place.
CompositionLocalOf
This type creates a CompositionLocal that changes happens to the value provided at some level during the recomposition resulting in invalidating and recomposing only the content that reads the CompositionLocal’s value.
This can be extremely useful in cases in which the provided value changes frequently as it will be efficient in terms of performance as the recomposition will only happen to a specific part of the UI tree.
For example, Elevation values can change frequently based on the used theme, that’s why we should define it using CompositionLocalOf API.
data class Elevation(val card: Dp = 6.dp, val button: Dp = 4.dp, val default: Dp = 2.dp) // Define a CompositionLocal top-level object with a default value val LocalElevations = compositionLocalOf { Elevations() }
staticCompositionLocalOf
This type also creates a CompositionLocal, but behaves differently as changing the value provided at some level will invalidate and recompose the entire UI tree below it even if it doesn’t use it!
This is only useful in case the provided value is highly unlikely to change or doesn’t change at all, otherwise, it will cause serious performance issues for your app!
For example, we can create a Dimensions class that we want to provide to our UI tree, as the dimensions are unlikely to change, we can use the staticCompositionLocalOf API.
data class Dimensions(val pagePadding: Dp = 16.dp, val verticalPadding: Dp = 12.dp, val default: Dp = 8.dp) // Define a CompositionLocal top-level object with a default value val LocalDimensions = staticCompositionLocalOf { Dimensions() }
Finally, How can we retrieve & provide values?
Retrieving the current value that the CompositionLocal
holds is straightforward using the current
property, which can’t be reassigned as it’s defined as val
and only accessible within Composable Context
.
The only way you can change the current value of the CompositionLocal
is by using its infix function provides
, and then provides the new value that will be available to all UI nodes under that scope!
Using the mentioned LocalElevations
, here is an example of how we can achieve the retrieve and provide functionalities:
@Composable
fun NavigationHolder() {
MyAppTheme {
...
composable(route = "home") {
// before navigating to home, provides a new value for button elevation
CompositionLocalProvider(
LocalElevations provides Elevations(button = 8.dp),
) {
HomeScreen()
}
}
}
}
@Composable
fun HomeScreen() {
val currentButtonElevation = LocalElevations.current.button // It's 8.dp
}
@Composable
fun DetailsScreen() {
val currentButtonElevation = LocalElevations.current.button // It's 4.dp which is the default value
}
But you should remember, an API exists DOESN'T mean we MUST use it, consider checking out these alternatives for specific use cases that you might be interested to check before jumping in using the CompositionLocal, from here :
Some alternatives to using CompositionLocal API.
And that's it for today, please follow if you find this helpful, so that you get the latest articles and updates!