当滚动时,LazyColumn无法保持项目的状态

13

在跟随谷歌关于Jetpack Compose的Pathway代码实验室时,我正在尝试这段代码。

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           MyApp {
               MyScreenContent()
           }
       }
   }
}

@Composable
fun MyApp(content: @Composable () -> Unit) {
   BasicsCodelabTheme {
       Surface(color = Color.Yellow) {
           content()
       }
   }
}

@Composable
fun MyScreenContent(names: List<String> = List(1000) { "Hello Android #$it" }) {
   val counterState = remember { mutableStateOf(0) }

   Column(modifier = Modifier.fillMaxHeight()) {
       NameList(names, Modifier.weight(1f))
       Counter(
           count = counterState.value,
           updateCount = { newCount ->
               counterState.value = newCount
           }
       )
   }
}

@Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
   LazyColumn(modifier = modifier) {
       items(items = names) { name ->
           Greeting(name = name)
           Divider(color = Color.Black)
       }
   }
}

@Composable
fun Greeting(name: String) {
   var isSelected by remember { mutableStateOf(false) }
   val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

   Text(
       text = "Hello $name!",
       modifier = Modifier
           .padding(24.dp)
           .background(color = backgroundColor)
           .clickable(onClick = { isSelected = !isSelected })
   )
}

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
   Button(
       onClick = { updateCount(count + 1) },
       colors = ButtonDefaults.buttonColors(
           backgroundColor = if (count > 5) Color.Green else Color.White
       )
   ) {
       Text("I've been clicked $count times")
   }
}

@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
   MyApp {
       MyScreenContent()
   }
}

我注意到LazyColumn在屏幕上变为可见时会重新组合项目(预期行为!),但是Greeting小部件的本地状态完全丢失了!
我认为这是Compose中的一个错误,理想情况下,composer应该考虑remember缓存的状态。有没有一种优雅的方法来解决这个问题?
提前感谢!

1
“Greeting”小部件的本地状态完全丢失,我假设您指的是isSelected和/或backgroundColor。 “我认为这是Compose中的一个错误” - 我怀疑。 remember()仅为单个组合(即特定调用可组合)记住,直到该组合被处理为止。 “Lazy”的重点在于,这些组合不会保留,因为它们滚动出视图,以最小化内存消耗。 “有没有一种优雅的方法来解决这个问题?” - 将状态提升出Greeting(),或者使用Column()并支付内存价格。 - CommonsWare
remember() 会为每个组合(即特定的可组合调用)记住状态,直到该组合被处理。这似乎是我想要的答案!非常感谢。我认为将状态提升到调用者范围是“优雅”的方式。我很快就会发布我的代码!非常感谢 :) - AouledIssa
1个回答

24

[更新]

使用rememberSaveable {...}可以使应用程序在Android配置更改以及scrollability时具有存活能力。

文档

Remember the value produced by init.
It behaves similarly to remember, but the stored value will survive the activity or process recreation using the saved instance state mechanism (for example it happens when the screen is rotated in the Android application).

现在的代码更加优雅和简短,我们甚至不需要提升状态,它可以保持内部。我现在唯一不确定使用rememberSaveable时是否会有性能惩罚,当列表变得越来越大,比如1000个项目。

@Composable
fun Greeting(name: String) {
    val isSelected = rememberSaveable { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isSelected.value) Color.Red else Color.Transparent)

    Text(
        text = "Hello $name! selected: ${isSelected.value}",
        modifier = Modifier
            .padding(24.dp)
            .background(color = backgroundColor)
            .clickable(onClick = {
                isSelected.value = !isSelected.value
            })
    )
}

[Translated Answer]

根据@CommonWare的回答,当LazyColumn中的组件离开屏幕时,它们及其状态将被处理。这意味着当LazyColumn再次重组Compsoables时,它会具有新的state。要解决此问题,需要将状态提升到使用者的范围内,即LazyColumn

另外,在remember { ... } lambda中,我们需要使用mutableStateMapOf()而不是MutableMapOf,否则Compose核心引擎将无法感知到此更改。

到目前为止,这是代码:

@Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {

    val selectedStates = remember {
        mutableStateMapOf<Int, Boolean>().apply {
            names.mapIndexed { index, _ ->
                index to false
            }.toMap().also {
                putAll(it)
            }
        }
    }

    LazyColumn(modifier = modifier) {
        itemsIndexed(items = names) { index, name ->
            Greeting(
                name = name,
                isSelected = selectedStates[index] == true,
                onSelected = {
                    selectedStates[index] = !it
                }
            )
            Divider(color = Color.Black)
        }
    }
}

愉快地创作吧!


1
rememberSaveable 是一个有趣的解决方案。正如你所说,了解它在大型列表中的表现将非常有趣。 - Florian Walther
@Aouledissa,这部分代码 selectedStates[index] = !it 有些令人困惑。 - binrebin

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