简体   繁体   中英

Jetpack Compose Smart Recomposition

I'm doing experiments to comprehend recomposition and smart recomposition and made a sample

在此处输入图像描述

Sorry for the colors, they are generated with Random.nextIn() to observe recomposition visually, setting colors has no effect on recomposition, tried without changing colors either.

What's in gif is composed of three parts

Sample1

@Composable
private fun Sample1() {

    Column(
        modifier = Modifier
            .background(getRandomColor())
            .fillMaxWidth()
            .padding(4.dp)
    ) {
        var counter by remember { mutableStateOf(0) }


        Text("Sample1", color = getRandomColor())

        Button(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 4.dp),
            colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
            onClick = {
                counter++
            }) {
            Text("Counter: $counter", color = getRandomColor())
        }
    }
}

I have no questions here since smart composition works as expected, Text on top is not reading changes in counter so recomposition only occurs for Text inside Button .

Sample2

@Composable
private fun Sample2() {
    Column(
        modifier = Modifier.background(getRandomColor())
    ) {

        var update1 by remember { mutableStateOf(0) }
        var update2 by remember { mutableStateOf(0) }

        println("ROOT")
        Text("Sample2", color = getRandomColor())

        Button(
            modifier = Modifier
                .padding(start = 8.dp, end = 8.dp, top = 4.dp)
                .fillMaxWidth(),
            colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
            onClick = {
                update1++
            },
            shape = RoundedCornerShape(5.dp)
        ) {

            println("🔥 Button1️")

            Text(
                text = "Update1: $update1",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )

        }

        Button(
            modifier = Modifier
                .padding(start = 8.dp, end = 8.dp, top = 2.dp)
                .fillMaxWidth(),
            colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
            onClick = { update2++ },
            shape = RoundedCornerShape(5.dp)
        ) {
            println("🍏 Button 2️")

            Text(
                text = "Update2: $update2",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )
        }

        Column(
            modifier = Modifier.background(getRandomColor())
        ) {

            println("🚀 Inner Column")
            var update3 by remember { mutableStateOf(0) }

            Button(
                modifier = Modifier
                    .padding(start = 8.dp, end = 8.dp, top = 2.dp)
                    .fillMaxWidth(),
                colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
                onClick = { update3++ },
                shape = RoundedCornerShape(5.dp)
            ) {

                println("✅ Button 3️")
                Text(
                    text = "Update2: $update2, Update3: $update3",
                    textAlign = TextAlign.Center,
                    color = getRandomColor()
                )

            }
        }

        Column() {
            println("☕️ Bottom Column")
            Text(
                text = "Sample2",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )
        }

    }
}

It also works as expected each mutableState is updating only the scope they have been observed in. Only Text that observes update2 and update3 is changed when either of these mutableStates are updated.

Sample3

@Composable
private fun Sample3() {
    Column(
        modifier = Modifier.background(getRandomColor())
    ) {


        var update1 by remember { mutableStateOf(0) }
        var update2 by remember { mutableStateOf(0) }


        println("ROOT")
        Text("Sample3", color = getRandomColor())

        Button(
            modifier = Modifier
                .padding(start = 8.dp, end = 8.dp, top = 4.dp)
                .fillMaxWidth(),
            colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
            onClick = {
                update1++
            },
            shape = RoundedCornerShape(5.dp)
        ) {

            println("🔥 Button1️")

            Text(
                text = "Update1: $update1",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )

        }

        Button(
            modifier = Modifier
                .padding(start = 8.dp, end = 8.dp, top = 2.dp)
                .fillMaxWidth(),
            colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
            onClick = { update2++ },
            shape = RoundedCornerShape(5.dp)
        ) {
            println("🍏 Button 2️")

            Text(
                text = "Update2: $update2",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )
        }

        Column {

            println("🚀 Inner Column")
            var update3 by remember { mutableStateOf(0) }

            Button(
                modifier = Modifier
                    .padding(start = 8.dp, end = 8.dp, top = 2.dp)
                    .fillMaxWidth(),
                colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
                onClick = { update3++ },
                shape = RoundedCornerShape(5.dp)
            ) {

                println("✅ Button 3️")
                Text(
                    text = "Update2: $update2, Update3: $update3",
                    textAlign = TextAlign.Center,
                    color = getRandomColor()
                )

            }
        }
       // 🔥🔥 Reading update1 causes entire composable to recompose
        Column(
            modifier = Modifier.background(getRandomColor())
        ) {
            println("☕️ Bottom Column")
            Text(
                text = "Update1: $update1",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )
        }
    }
}

Only difference between Sample2 and Sample3 is Text at the bottom is reading update1 mutableState which causing entire composable to be recomposed. As you can see in gif changing update1 recomposes or changes entire color schema for Sample3.

What's the reason for recomposing entire composable?

        Column(
            modifier = Modifier.background(getRandomColor())
        ) {
            println("☕️ Bottom Column")
            Text(
                text = "Update1: $update1",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )
        }
    }

To have smart recomposition scopes play a pivotal role. You can check Vinay Gaba's What is “donut-hole skipping” in Jetpack Compose? article.

Leland Richardson explains in this tweet as

The part that is "donut hole skipping" is the fact that a new lambda being passed into a composable (ie Button) can recompose without recompiling the rest of it. The fact that the lambda are recompose scopes are necessary for you to be able to do this, but not sufficient

In other words, composable lambda are "special":)

We wanted to do this for a long time but thought it was too complicated until @chuckjaz had the brilliant realization that if the lambdas were state objects, and invokes were reads, then this is exactly the result

You can also check other answers about smart recomposition here , and here .

https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78

When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit. Column, Row and Box are inline functions and because of that they don't create scopes.

Created RandomColorColumn that take other Composables and its scope content: @Composable () -> Unit

@Composable
fun RandomColorColumn(content: @Composable () -> Unit) {

    Column(
        modifier = Modifier
            .padding(4.dp)
            .shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp))
            .background(getRandomColor())
            .padding(4.dp)
    ) {
        content()
    }
}

And replaced

 Column(
        modifier = Modifier.background(getRandomColor())
    ) {
        println("☕️ Bottom Column")
        Text(
            text = "Update1: $update1",
            textAlign = TextAlign.Center,
            color = getRandomColor()
        )
    }
}

with

    RandomColorColumn() {

        println("☕️ Bottom Column")
        /*
            🔥🔥 Observing update(mutableState) does NOT causes entire composable to recompose
         */
        Text(
            text = "🔥 Update1: $update1",
            textAlign = TextAlign.Center,
            color = getRandomColor()
        )
    }
}

Only this scope gets updated as expected and we have smart recomposition.

在此处输入图像描述

What causes Text , or any Composable, inside Column to not have a scope, thus being recomposed when a mutableState value changes is Column having inline keyword in function signature.

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

If you add inline to RandomColorColumn function signature you will see that it causes whole Composable to recompose .

Compose uses call sites defined as

The call site is the source code location in which a composable is called. This influences its place in Composition, and therefore, the UI tree.

If during a recomposition a composable calls different composables than it did during the previous composition, Compose will identify which composables were called or not called and for the composables that were called in both compositions, Compose will avoid recomposing them if their inputs haven't changed.

Consider the following example:

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

Call site of a Composable function affects smart recomposition, and having inline keyword in a Composable sets its child Composables call site same level, not one level below.

For anyone interested here is the github repo to play/test recomposition

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM