Jetpack Compose Scoped/Smart Recomposition

15
我正在进行实验,以理解重组和智能重组,并制作了一个样本。

enter image description here

抱歉颜色不好看,它们是用Random.nextIn()生成的,目的是为了观察重新组合的效果,设置颜色对重新组合没有影响,即使不改变颜色也试过了。
gif中包含了三个部分。 示例1
@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())
        }
    }
}

我这里没有问题,因为智能组合按预期工作,顶部的Text没有读取counter的变化,所以只有Button内部的Text会重新组合。 示例2
@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()
            )
        }

    }
}

它也按预期工作,每个可变状态只会在其被观察的范围内更新。只有观察了update2update3Text在这些可变状态更新时才会改变。 示例3
@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()
            )
        }
    }
}

只有Sample2Sample3之间的区别是底部的Text正在读取update1 mutableState,这导致整个组合重新组合。如您在gif中所见,更改update1会重新组合或更改Sample3的整个颜色方案。
为什么要重新组合整个组合?
        Column(
            modifier = Modifier.background(getRandomColor())
        ) {
            println("☕️ Bottom Column")
            Text(
                text = "Update1: $update1",
                textAlign = TextAlign.Center,
                color = getRandomColor()
            )
        }
    }

请问您如何测试代码中的重构是否发生?有没有工具或只能通过日志来测试? - Sepideh Vatankhah
在我发布的答案中的文章中,有一个带有SideEffect的日志代码,您可以计算重新组合的次数。您可以在测试中使用assertEquals检查预期的数量。 - Thracian
1个回答

24

在Jetpack Compose中,智能重组作用域发挥了关键作用。您可以查看Vinay Gaba的文章“donut-hole skipping”是什么?

Leland Richardson在这个推文中解释道:

所谓“甜甜圈跳过”,就是新的lambda(即Button)被传递到可组合对象中时,可以重新组合而无需重新编译其余部分。这些lambda是重新组合作用域必不可少的,但并不足够。

换句话说,可组合lambda是“特殊”的 :)

我们想做这件事情已经很长时间了,但认为它太复杂,直到@chuckjaz有了一个非常棒的想法:如果lambda是状态对象,并且调用是读取操作,那么这正是我们想要的结果。

您也可以在这里这里查看有关智能重组的其他答案。

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

当读取一个 State 时,会在最近的作用域中触发重新组合。而作用域是指未标记为 inline 并返回 Unit 的函数。Column、Row 和 Box 是内联函数,因此它们不创建作用域。

创建了 RandomColorColumn,它接受其他 Composables 并具有作用域 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()
    }
}

并替换

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

使用

    RandomColorColumn() {

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

只有此范围按预期更新,我们拥有智能重组。

enter image description here

Column内的Text或任何可组合项没有作用域,因此当可变状态值更改时会重新组合的原因是函数签名中带有inline关键字。

@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
    )
}

如果在RandomColorColumn函数签名中添加inline,你会发现它会导致整个可组合重新组合

Compose使用定义为调用点的内容

调用点是可组合被调用的源代码位置。这影响它在组合中的位置,从而影响UI树。

如果在重新组合期间,可组合调用了与之前不同的可组合,则Compose将识别哪些可组合已被调用或未被调用,并且对于在两个组合中都被调用的可组合,如果它们的输入没有改变,Compose将避免重新组合它们。

考虑以下示例:

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

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

可组合函数的调用位置会影响智能重组,而在可组合函数中使用inline关键字可以将其子组件的调用位置设置为与同级别,而不是低一级别。

对于任何有兴趣的人,这里是github repo来进行重组测试。


3
关于内联关键字的信息和文章真的很有帮助,谢谢你提到它! - Dominik Seemayr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接