Android Compose 中的哑重组

Posted

技术标签:

【中文标题】Android Compose 中的哑重组【英文标题】:Dumb recomposition in Android Compose 【发布时间】:2022-01-13 22:58:35 【问题描述】:

考虑这个最小的代码 sn-p(在 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 不断变化)。

    这是为什么呢?

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

【问题讨论】:

【参考方案1】:

你可以试试运行这段代码

@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/函数。因此,IdempotenceTest 函数(在我的示例中和您的 setContent lamda 中)将被重新执行,它将调用 getRandomUuidreturnSameValue 函数,并根据这些返回的值决定是否重新组合依赖于这些返回值的元素。如果您想防止计算一次又一次地发生,请将其包装在 remember 块中。

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

附带说明,可组合函数必须是无副作用的,以确保幂等性。你可以阅读更多here。

要了解更多关于重组范围的信息,您可以参考博文here 和here。

【讨论】:

“每次 Column 重组”..这是问题的本质......为什么整个 Column 首先要重组? "编译器会调用 getRandomUuid..." 为什么?编译器不应该只调用观察time 的代码吗? 我已编辑答案以包含有关重组范围的更多信息。 扎克的帖子真的把事情弄清楚了..thnx【参考方案2】:

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)

【讨论】:

以上是关于Android Compose 中的哑重组的主要内容,如果未能解决你的问题,请参考以下文章

Jetpack Compose 中的重组作用域和性能优化

Kotlin Compose 重组

Jetpack Compose – LazyColumn 不重组

Jetpack Compose中的副作用Api

在 React 中的哑组件内部映射/循环

Jetpact Compose状态管理简单理解