Android Compose 中的哑重组

Dumb recomposition in Android Compose

考虑这个最小的代码片段(在 Kotlin 中):

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import java.time.LocalDateTime
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            var time by remember {
                mutableStateOf("time")
            }
            Column(modifier = Modifier.clickable { time = LocalDateTime.now().toString() }) {
                Text(text = UUID.randomUUID().toString())
                Text(text = time)
            }
        }
    }
}

检查上面的代码,从逻辑的角度来看,人们期望在单击 Column 时,因为只有参数 time 发生变化,所以只有较低的时间 Text 可组合项是重新绘制。这是因为 recomposition skips as much as possible.

然而,发现上面的 Text 可组合项也在重绘(显示的 UUID 一直在变化)。

  1. 这是为什么?

请注意,我的 Column 可组合项的非幂等性应该没有影响,除非重绘是愚蠢的。

你可以试试运行这段代码

@Composable
fun IdempotenceTest() {    
    var time by remember {
        mutableStateOf("time")
    }
    Column(
        modifier = Modifier.clickable {
            time = LocalDateTime.now().toString()
        }
    ) {
        Text(text = getRandomUuid())
        TestComposable(text = returnSameValue())
        Text(text = time)
    }
}

@Composable
fun TestComposable(text: String) {
    SideEffect {
        Log.d(TAG, "TestComposable composed with: $text")
    }
    Text(text = text)
}

private fun getRandomUuid(): String {
    Log.d(TAG, "getRandomUuid: called")
    return UUID.randomUUID().toString()
}

private fun returnSameValue(): String {
    Log.d(TAG, "returnSameValue: called")
    return "test"
}

如果您查看日志,您会发现每次状态更改时,编译器都会尝试重新调用正在读取状态值的最小包围 lamda/function。因此,IdempotenceTest 函数(在我的例子中和 setContent{} lamda 在你的例子中)将被重新执行,这将调用 getRandomUuidreturnSameValue 函数并基于 returned 的值这些,它将决定是否重新组合依赖于这些 return 值的元素。如果你想防止计算一次又一次地发生,把它包装在一个 remember{} 块中。

现在,如果您要使用 Button 来代替列,您会发现只有相同的内容 lamda 会被执行。发生这种情况的原因是 Column 是一个内联函数,而 Button 本身在其内部使用非内联的 Surface。因此 Column{} 的内容被复制到封闭的函数块中,从而导致整个 IdempotenceTest 因重新组合而无效。

作为旁注,可组合函数必须无副作用以确保幂等性。您可以阅读更多 here.

要了解有关重组作用域的更多信息,您可以参考博文 here and here

rememberSavable {...} 为我工作。

它允许您直接从 clickable 更新状态并触发重组:

...
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
...

var time by rememberSaveable { mutableStateOf("time") }
Column(
    modifier = Modifier.clickable { 
        time = LocalDateTime.now().toString() 
    }
) {
    Text(text = UUID.randomUUID().toString())
    Text(text = time)
}