A Jetpack Compose Composition Local Tutorial

We already know from previous chapters that user interfaces are built in Compose by constructing hierarchies of composable functions. We also know that Compose is state-driven and that state should generally be declared in the highest possible node of the composable tree (a concept referred to as state hoisting) and passed down through the hierarchy to the descendant composables where it is needed. While this works well for most situations, it can become cumbersome if the state needs to be passed down through multiple levels within the hierarchy. A solution to this problem exists in the form of CompositionLocal, which is the subject of this chapter.

Understanding CompositionLocal

In simple terms, CompositionLocal provides a way to make state declared higher in the composable hierarchy tree available to functions lower in the tree without having to pass it through every composable between the point where it is declared and the function where it is used. Consider, for example, the following hierarchy diagram:

Figure 21-1

In the hierarchy, a state named colorState is declared in Composable1 but is only used in Composable8. Although the state is not needed in either Composable3 or Composable5, colorState still needs to be passed down through those functions to reach Composable8. The deeper the tree becomes, the more levels through which the state needs to be passed to reach the function where it is used.

A solution to this problem is to use CompositionLocal. CompositionLocal allows us to declare the data at the highest necessary node in the tree and then access it in descendants without having to pass it through the intervening children as shown in Figure 21-2:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

Figure 21-2

CompositionLocal has the added advantage of only making the data available to the branch of the tree below the point at which it is assigned a value. In other words, if the state were assigned a value when calling composable3 it will be accessible within composable numbers 3, 5, 7, and 8, but not to composables 1, 2, 4, or 6. This allows state to be kept local to specific branches of the composable tree, and also for different sub-branches to have different values assigned to the same CompositionLocal state. Composable5 could, for example, have a different color assigned to colorState from that assigned when Composable7 is called.

Using CompositionLocal

Declaring state using CompositionLocal starts with the creation of a ProvidableCompositionLocal instance which can be obtained via a call to either the compositionLocalOf() or staticCompositionLocalOf() function. In each case the function accepts a lambda defining a default value to be assigned to the state in the absence of a specific assignment, for example:

val LocalColor = compositionLocalOf { Color.Red } 
val LocalColor = staticCompositionLocalOf { Color.Red }

The staticCompositionLocalOf() function is recommended for storing state values that are unlikely to change very often. The reason for this is that any changes to the state value will cause the entire tree beneath where the value is assigned to be recomposed. The compositionLocalOf() function, on the other hand, will only cause recomposition to be performed on composables where the current state is accessed. This function should be used when dealing with states that change frequently.

The next step is to assign a value to the ProvidableCompositionLocal instance and wrap the call to the immediate descendant child composable in a CompositionLocalProvider call:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

val color = Color.Blue
 
CompositionLocalProvider(LocalColor provides color) {
    Composable5()
}

Any descendants of Composition5 will now be able to access the CompositionLocal state via the current property of the ProviderCompositionLocal instance, for example:

val background = LocalColor.current

In the rest of this chapter, we will build a project that mirrors the hierarchy illustrated in Figure 21-1 to show CompositionLocal in action.

Creating the CompLocalDemo project

Launch Android Studio and create a new Empty Compose Activity project named CompLocalDemo, specifying com.example.complocaldemo as the package name, and selecting a minimum API level of API 26: Android 8.0 (Oreo). Within the MainActivity.kt file, delete the Greeting function and add a new empty composable named Composable1:

@Composable
fun Composable1() {
    
}

Next, edit the onCreateActivity() method and DefaultPreview function to call Composable1 instead of Greeting.

Designing the layout

Within the MainActivity.kt file, implement the composable hierarchy as follows:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

.
.
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
.
.
@Composable
fun Composable1() {
    Column {
        Composable2()
        Composable3()
    }
}
 
@Composable
fun Composable2() {
    Composable4()
}
 
@Composable
fun Composable3() {
    Composable5()
}
 
@Composable
fun Composable4() {
    Composable6()
}
 
@Composable
fun Composable5() {
    Composable7()
    Composable8()
}
 
@Composable
fun Composable6() {
    Text("Composable 6")
}
 
@Composable
fun Composable7() {
 
}
 
@Composable
fun Composable8() {
    Text("Composable 8")
}

Adding the CompositionLocal state

The objective for this project is to declare a color state that can be changed depending on whether the device is in light or dark mode, and use that to control the background color of the text component in Composable8. Since this is a value that will not change regularly, we can use the staticCompositionLocalOf() function. Remaining within the MainActivity.kt file, add the following line above the Composable1 declaration:

.
.
val LocalColor = staticCompositionLocalOf { Color(0xFFffdbcf) }
 
@Composable
fun Composable1() {
    Column {
.
.

Next, a call to isSystemInDarkTheme() needs to be added, and the result used to assign a different color to the LocalColor state. We also need to call Composable3 from within the context of the CompositionLocal provider:

@Composable
fun Composable1() {
 
    var color = if (isSystemInDarkTheme()) {
        Color(0xFFa08d87)
    } else {
        Color(0xFFffdbcf)
    }
 
    Column {
        Composable2()
 
        CompositionLocalProvider(LocalColor provides color) {
            Composable3()
        }      
    }
}

Accessing the CompositionLocal state

The final task before testing the code is to assign the color state to the Text component in Composable8 as follows:

@Composable
fun Composable8() {
    Text("Composable 8", modifier = Modifier.background(LocalColor.current))
}

Testing the design

To test the activity code in both light and dark modes, add a new Preview composable to MainActivity.kt with uiMode set to UI_NIGHT_MODE_YES:

.
.
import android.content.res.Configuration.UI_MODE_NIGHT_YES
.
.
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun DarkPreview() {
    CompLocalDemoTheme {
        Composable1()
    }
}

After refreshing the Preview panel, both the default and dark preview should appear, each using a different color as the background for the Text component in Composable8:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

Figure 21-3

We can also modify the code so that composables 3, 5, 7, and 8 all have a different color setting. All this requires is calling each composable from within a CompositionLocalProvider with a different color assignment:

.
.
@Composable
fun Composable3() {
 
    Text("Composable 3", modifier = Modifier.background(LocalColor.current))
 
    CompositionLocalProvider(LocalColor provides Color.Red) {
        Composable5()
    }
}
.
.
@Composable
fun Composable5() {
 
    Text("Composable 5", modifier = Modifier.background(LocalColor.current))
 
    CompositionLocalProvider(LocalColor provides Color.Green) {
        Composable7()
    }
 
    CompositionLocalProvider(LocalColor provides Color.Yellow) {
        Composable8()
    }
}
.
.
@Composable
fun Composable7() {
    Text("Composable 7", modifier = Modifier.background(LocalColor.current))
}
.
.

Now when the Preview panel is refreshed, all four components will have a different color, all based on the same LocalColor state:

Figure 21-4

As one final step, try to access the LocalColor state from Composable6:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

@Composable
fun Composable6() {
    Text("Composable 6", modifier = Modifier.background(LocalColor.current))
}

On refreshing the preview the Text component for Compsoable6 will appear using the default color assigned to LocalColor. This is because Composable6 is in a different branch of the tree and does not have access to the current LocalColor setting.

Summary

This chapter has introduced CompositionLocal and demonstrated how it can be used to declare state that is accessible to composables lower down in the layout hierarchy without having to be passed from one child to another. State declared in this way is local to the branch of the hierarchy tree in which a value is assigned.