Search code examples
kotlinuser-interfaceandroid-jetpack-composecompose-desktop

How and when exactly CompositionLocal sets values implicitly?


I'm trying to understand how CompositionLocal actually sets values implicitly and what requiremnts need to be met so that it works, but Android's documentation about Locally scoped data with CompositionLocal isn't helping.

There is an example where the color of a Text is being changed by assigning a new value to the color parameter.

// Some composable deep in the hierarchy of MaterialTheme
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // `primary` is obtained from MaterialTheme's
        // LocalColors CompositionLocal
        color = MaterialTheme.colors.primary
    )
}

But this is not implicit if you need to set it this way!

Then they show another example where they change the ContentAlpha via the CompositionLocalProvider and here the Text suddenly can use the new value implicitly even though they write that CompositionLocal is what the Material theme uses under the hood. so why doesn't it work with the first example?

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha now")
}

There's also a 3rd example where they show how to create your own CompositionLocal, but here agian, they explicitly set the Card's elevation parameter!

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    Card(elevation = LocalElevations.current.card) {
        // Content
    }
}

Didn't they just create the CompositionLocalProvider to avoid doing this?

            // Bind elevation as the value for LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // This part of Composition will see the `elevations` instance
                // when accessing LocalElevations.current
            }

Solution

  • @Composable
    fun CompositionLocalExample() {
        MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
            Column {
                Text("Uses MaterialTheme's provided alpha")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                    Text("Medium value provided for LocalContentAlpha")
                    Text("This Text also uses the medium value")
                    CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                        DescendantExample()
                    }
                }
            }
        }
    }
    

    In this sample Text sets alpha in source code using LocalContentAlpha.current value with

    val localContentColor = LocalContentColor.current
    val localContentAlpha = LocalContentAlpha.current
    val overrideColorOrUnspecified: Color = if (color.isSpecified) {
        color
    } else if (style.color.isSpecified) {
        style.color
    } else {
        localContentColor.copy(localContentAlpha)
    }
    

    because of that changing value LocalContentAlpha provides changes the alpha Text gets.

    As in third example where elevation is set with

    Consuming the CompositionLocal CompositionLocal.current returns the value provided by the nearest CompositionLocalProvider that provides a value to that CompositionLocal:

    @Composable
    fun SomeComposable() {
        // Access the globally defined LocalElevations variable to get the
        // current Elevations in this part of the Composition
        Card(elevation = LocalElevations.current.card) {
            // Content
        }
    }
    

    Also for instance with Icon which has default tint

    tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
    

    You can apply LocalContentAlpha or LocalContentColor to change default tint value or explicitly change param itself.

    @Preview
    @Composable
    private fun Test() {
    
        Column(
            Modifier
                .fillMaxSize()
                .padding(10.dp)
        ) {
    
            // Default icon
            Icon(
                imageVector = Icons.Default.Favorite,
                contentDescription = null
            )
            Spacer(modifier = Modifier.height(10.dp))
    
            Icon(
                imageVector = Icons.Default.Favorite,
                contentDescription = null,
                tint = Color.Green
            )
            Spacer(modifier = Modifier.height(10.dp))
    
            Icon(
                modifier = Modifier.alpha(.3f),
                imageVector = Icons.Default.Favorite,
                tint = Color.Green,
                contentDescription = null
            )
            Spacer(modifier = Modifier.height(10.dp))
    
            // Icon uses tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
            // changing it also changes default tint
            CompositionLocalProvider(LocalContentColor provides Color.Green) {
                Icon(
                    imageVector = Icons.Default.Favorite,
                    contentDescription = null
                )
            }
            Spacer(modifier = Modifier.height(10.dp))
    
            CompositionLocalProvider(LocalContentAlpha provides .3f) {
                CompositionLocalProvider(LocalContentColor provides Color.Green) {
                    Icon(
                        imageVector = Icons.Default.Favorite,
                        contentDescription = null
                    )
                }
            }
        }
    }