MutableState使用结构相等性来检查是否使用新实例更新state.value。每次选择新项目时,您都会创建一个新的列表实例。
您可以使用SnapshotStateList,在添加、删除或使用新实例更新现有项目时触发重新组合。 SnapshotStateList是一个列表,通过时间复杂度为O(1)的方式获取项目,而不是在最坏情况下使用O(n)的方式遍历整个列表。
仅使用mutableStateListOf
结果只有单个项目被重新组合。
data class Person(val id: Int, val name: String, val isSelected: Boolean = false)
你可以使用SnapshotState列表来更新你的ViewModel。
class MyViewModel : ViewModel() {
private val initialList = listOf(
Person(id = 0, name = "Name0"),
Person(id = 1, name = "Name1"),
Person(id = 2, name = "Name2"),
Person(id = 3, name = "Name3"),
Person(id = 4, name = "Name4"),
Person(id = 5, name = "Name5"),
Person(id = 6, name = "Name6"),
)
val people = mutableStateListOf<Person>().apply {
addAll(initialList)
}
fun toggleSelection(index: Int) {
val item = people[index]
val isSelected = item.isSelected
people[index] = item.copy(isSelected = !isSelected)
}
}
ListItem
可组合
@Composable
private fun ListItem(item: Person, onItemClick: (Int) -> Unit) {
Column(
modifier = Modifier.border(3.dp, randomColor())
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemClick(item.id)
}
.padding(8.dp)
) {
Text("Index: Name ${item.name}", fontSize = 20.sp)
if (item.isSelected) {
Icon(
modifier = Modifier
.align(Alignment.CenterEnd)
.background(Color.Red, CircleShape),
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
)
}
}
}
}
您的清单
@Composable
fun ListScreen(people: List<Person>, onItemClick: (Int) -> Unit) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.fillMaxSize()
) {
items(items = people, key = { it.hashCode() }) {
ListItem(item = it, onItemClick = onItemClick)
}
}
}
我用于视觉检查重组的代码
fun randomColor() = Color(
Random.nextInt(256),
Random.nextInt(256),
Random.nextInt(256),
alpha = 255
)
使用ViewState
结果
sealed class ViewState {
object Loading : ViewState()
data class Success(val data: List<Person>) : ViewState()
}
更新ViewModel如下:
class MyViewModel : ViewModel() {
private val initialList = listOf(
Person(id = 0, name = "Name0"),
Person(id = 1, name = "Name1"),
Person(id = 2, name = "Name2"),
Person(id = 3, name = "Name3"),
Person(id = 4, name = "Name4"),
Person(id = 5, name = "Name5"),
Person(id = 6, name = "Name6"),
)
private val people: SnapshotStateList<Person> = mutableStateListOf<Person>()
var viewState by mutableStateOf<ViewState>(ViewState.Loading)
private set
init {
viewModelScope.launch {
delay(1000)
people.addAll(initialList)
viewState = ViewState.Success(people)
}
}
fun toggleSelection(index: Int) {
val item = people[index]
val isSelected = item.isSelected
people[index] = item.copy(isSelected = !isSelected)
viewState = ViewState.Success(people)
}
}
1000毫秒和延迟是为了演示。在真实的应用程序中,您将从REST或数据库获取数据。
屏幕显示列表或使用ViewState加载。
@Composable
fun ListScreen(
viewModel: MyViewModel,
onItemClick: (Int) -> Unit
) {
val state = viewModel.viewState
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (state) {
is ViewState.Success -> {
val people = state.data
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.fillMaxSize()
) {
items(items = people, key = { it.id }) {
ListItem(item = it, onItemClick = onItemClick)
}
}
}
else -> {
CircularProgressIndicator()
}
}
}
}
稳定性编辑
首先,当您滚动超出视口的项目并且它们重新进入视口时,它们会重新组合,这就是LazyColumn的工作原理,也是为什么它与垂直滚动的Column相比重新组合更少的项目。它重新组合可见的项目和滚动方向上的项目。
要显示如果您按照上述方式实现代码,则除非在您的实现中存在项目的稳定性问题,否则项目不会重新组合。
如果您在SideEffect
中看不到任何内容,那么无论布局检查器显示什么,该函数肯定不会重新组合。此外,当我们通过Modifier.background(getRandomColor)在Text
组件上调用新的修饰符时,组合体无法跳过重新组合,因此如果没有视觉变化,则不会重新组合。
下面的组合体返回稳定性:
restartable scheme("[androidx.compose.ui.UiComposable]") fun MainScreen(
unstable viewModel: MyViewModel
)
restartable scheme("[androidx.compose.ui.UiComposable]") fun ListScreen(
unstable people: List<Person>
stable onItemClick: Function1<Int, Unit>
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ListItem(
stable item: Person
stable onItemClick: Function1<Int, Unit>
)
restartable skippable scheme("[0, [0]]") fun StabilityTestTheme(
stable darkTheme: Boolean = @dynamic isSystemInDarkTheme($composer, 0)
stable dynamicColor: Boolean = @static true
stable content: Function2<Composer, Int, Unit>
)
注意:这是一个可重新启动和可跳过的可组合项,如果您的列表项正在重新组合,请确保您的可组合项的输入是稳定的。
@Composable
private fun ListItem(item: Person, onItemClick: (Int) -> Unit) {
SideEffect {
println("Recomposing ${item.id}, selected: ${item.isSelected}")
}
Column(
modifier = Modifier.border(3.dp, getRandomColor())
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemClick(item.id)
}
.padding(8.dp)
) {
Text("Index: Name ${item.name}", fontSize = 20.sp)
if (item.isSelected) {
Icon(
modifier = Modifier
.align(Alignment.CenterEnd)
.background(Color.Red, CircleShape),
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
)
}
}
}
}
ListScreen
Composable因为people: List<Person>
而不稳定,但只有在MainScreen
重新组合时才会重新组合。
@Composable
fun ListScreen(
people: List<Person>,
onItemClick: (Int) -> Unit
) {
SideEffect {
println("ListScreen is recomposing...$people")
}
Column {
Text(
text = "Header",
modifier = Modifier.border(2.dp, getRandomColor()),
fontSize = 30.sp
)
Spacer(modifier = Modifier.height(20.dp))
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.fillMaxSize()
.border(3.dp, getRandomColor(), RoundedCornerShape(8.dp))
) {
items(
items = people,
key = { it.hashCode() }
) {
ListItem(item = it, onItemClick = onItemClick)
}
}
}
}
并添加了一个按钮来安排重新组合,以显示当在MainScreenScope中触发重新组合时,ListScreen会被重新组合。
@Composable
fun MainScreen(
viewModel: MyViewModel
) {
var counter by remember {
mutableStateOf(0)
}
Column {
val people = viewModel.people
Text(text = "Counter $counter")
Button(onClick = { counter++ }) {
Text(text = "Increase Counter")
}
Spacer(modifier = Modifier.height(40.dp))
ListScreen(
people = people,
onItemClick = {
viewModel.toggleSelection(it)
}
)
}
}
你应该能够在布局检查器中看到,点击任何项目都会跳过其他项目,但点击Button
会重新组合ListScreen
和标题。
如果你向下滚动然后向上滚动,你会看到项目按预期重新进入组合。
正如你在gif中所看到的那样:
- 点击任何项目只会触发该项目的重新组合
- 点击Button会触发每个
ListItem
的重新组合
- 点击Button会触发
ListScreen
的重新组合
第二个问题发生在你可以看到的ViewModel不稳定并且调用viewModel.toggle()或viewModel::toggle是不稳定的。
稳定性也适用于lambda表达式或回调函数,你可以在这个示例中进行测试。
https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials/blob/master/Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/chapter4_state/Tutorial4_2_7LambdaRecomposition.kt
你可以将这个lambda函数保存在
remember
中。
val onClick = remember {
{ index: Int ->
viewModel.toggleSelection(index)
}
}
并调用ListScreen
ListScreen(
people = people,
onItemClick = onClick
)
现在你会看到,任何在
MainScreen
中触发的组合只有Text(header)和
ListScreen
会被组合,而不是ListItems。
最后一部分是使ListScreen稳定。如果你改变了
@Composable
fun ListScreen(
people: List<Person>,
onItemClick: (Int) -> Unit
)
到
@Composable
fun ListScreen(
people: SnapshotStateList<Person>,
onItemClick: (Int) -> Unit
)
你也可以参考这个答案
在Jetpack Compose中防止不必要的列表更新重新组合
当按钮或者在你的情况下可能是其他触发重新组合的东西时,不会重新组合任何内容。
如果你想测试,这是完整的演示。
class MainActivity : ComponentActivity() {
private val mainViewModel by viewModels<MyViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StabilityTestTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainScreen(mainViewModel)
}
}
}
}
}
@Composable
fun MainScreen(
viewModel: MyViewModel
) {
var counter by remember {
mutableStateOf(0)
}
val onClick = remember {
{ index: Int ->
viewModel.toggleSelection(index)
}
}
Column(
modifier = Modifier.padding(8.dp),
) {
val people = viewModel.people
Text(text = "Counter $counter")
Button(onClick = { counter++ }) {
Text(text = "Increase Counter")
}
Spacer(modifier = Modifier.height(40.dp))
ListScreen(
people = people,
onItemClick = onClick
)
}
}
@Composable
fun ListScreen(
people: SnapshotStateList<Person>,
onItemClick: (Int) -> Unit
) {
SideEffect {
println("ListScreen is recomposing...$people")
}
Column {
Text(
text = "Header",
modifier = Modifier.border(2.dp, getRandomColor()),
fontSize = 30.sp
)
Spacer(modifier = Modifier.height(20.dp))
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.fillMaxSize()
.border(3.dp, getRandomColor(), RoundedCornerShape(8.dp))
) {
items(
items = people,
key = { it.hashCode() }
) {
ListItem(item = it, onItemClick = onItemClick)
}
}
}
}
@Composable
private fun ListItem(item: Person, onItemClick: (Int) -> Unit) {
SideEffect {
println("Recomposing ${item.id}, selected: ${item.isSelected}")
}
Column(
modifier = Modifier.border(3.dp, getRandomColor())
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemClick(item.id)
}
.padding(8.dp)
) {
Text("Index: Name ${item.name}", fontSize = 20.sp)
if (item.isSelected) {
Icon(
modifier = Modifier
.align(Alignment.CenterEnd)
.background(Color.Red, CircleShape),
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
)
}
}
}
}
data class Person(val id: Int, val name: String, val isSelected: Boolean = false)
class MyViewModel : ViewModel() {
private val initialList = List(30) { index: Int ->
Person(id = index, name = "Name$index")
}
val people = mutableStateListOf<Person>().apply {
addAll(initialList)
}
fun toggleSelection(index: Int) {
val item = people[index]
val isSelected = item.isSelected
people[index] = item.copy(isSelected = !isSelected)
}
}
fun getRandomColor() = Color(
Random.nextInt(256),
Random.nextInt(256),
Random.nextInt(256),
alpha = 255
)
data class ViewState(val persons: List<Person> = emptyList(), any other properties)
private val viewState = MutableStateFlow(ViewState())
- pcparticleViewState
示例。 - Thraciankey = { it.hashCode() }
并使用SnapshotState处理该情况。您可以查看更新的答案的稳定性部分。 - Thracian