pcp*_*cle 11 android android-jetpack-compose compose-recomposition android-jetpack-compose-lazy-column
我正在尝试执行一些列表操作,但遇到了单个项目更新时所有项目重新组合的问题。
我的模特;
data class Person(val id: Int, val name: String, val isSelected: Boolean = false)
@Stable
data class PersonsWrapper(val persons: List<Person>)
Run Code Online (Sandbox Code Playgroud)
我的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 list = mutableStateOf(PersonsWrapper(initialList))
fun updateItemSelection(id: Int) {
val newList = list.value.persons.map {
if (it.id == id) {
it.copy(isSelected = !it.isSelected)
} else {
it
}
}
list.value = list.value.copy(persons = newList)
}
Run Code Online (Sandbox Code Playgroud)
和我的可组合函数;
@Composable
fun ListScreen(personsWrapper: PersonsWrapper, onItemClick: (Int) -> Unit) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.fillMaxSize()
) {
items(personsWrapper.persons, key = { it.id }) {
ListItem(item = it, onItemClick = onItemClick)
}
}
}
Run Code Online (Sandbox Code Playgroud)
所有模型类在 compose_reports 中看起来都很稳定;
stable class Person {
stable val id: Int
stable val name: String
stable val isSelected: Boolean
<runtime stability> = Stable
}
stable class PersonsWrapper {
unstable val persons: List<Person>
}
Run Code Online (Sandbox Code Playgroud)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ListScreen(
stable personsWrapper: PersonsWrapper
stable onItemClick: Function1<Int, Unit>
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ListItem(
stable item: Person
stable onItemClick: Function1<Int, Unit>
)
Run Code Online (Sandbox Code Playgroud)
当我想更改列表中单个项目的选定状态时,整个列表将被重新组合。我还尝试使用 kotlinx.collections 中的 ImmutableList 和 Persistant 列表。但问题并没有解决。
列表操作时如何避免不必要的重组?
Thr*_*ian 22
MutableState 使用结构相等来检查您是否使用新实例更新 state.value 。每次选择新项目时,您都会创建列表的新实例。
当您使用新实例添加、删除或更新现有项目时,您可以使用SnapshotStateList
哪个触发重组。SnapshotStateList 是一个列表,它获取项目的时间复杂度为 O(1),item[index]
而不是在最坏情况下使用 O(n) 迭代整个列表。
结果是只有单个项目被重组。
data class Person(val id: Int, val name: String, val isSelected: Boolean = false)
Run Code Online (Sandbox Code Playgroud)
您可以使用 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)
}
}
Run Code Online (Sandbox Code Playgroud)
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,
)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
你的清单
@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)
}
}
}
Run Code Online (Sandbox Code Playgroud)
我用于目视检查重组的代码
fun randomColor() = Color(
Random.nextInt(256),
Random.nextInt(256),
Random.nextInt(256),
alpha = 255
)
Run Code Online (Sandbox Code Playgroud)
结果
sealed class ViewState {
object Loading : ViewState()
data class Success(val data: List<Person>) : ViewState()
}
Run Code Online (Sandbox Code Playgroud)
并将 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)
}
}
Run Code Online (Sandbox Code Playgroud)
1000 ms 和延迟用于演示。在真实的应用程序中,您将从 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()
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
首先,当您将项目滚动到视口之外并且它们返回到视口中时,它们会被重新组合,这就是 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>
)
Run Code Online (Sandbox Code Playgroud)
注意:这是一个可重新启动且可跳过的可组合项,如果您的列表项正在重新组合,请确保可组合项的输入稳定。
@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,
)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
ListScreen
可组合性是不稳定的,因为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)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
并添加了一个按钮来安排重组,以显示在 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)
}
)
}
}
Run Code Online (Sandbox Code Playgroud)
您应该能够在布局检查器中看到,单击任何项目都会跳过其他项目,但单击Button
重组ListScreen
和标题。
如果您向下和向上滚动,您将看到项目按预期重新进入合成。
正如你在 gif 中看到的那样
ListItem
ListScreen
第二个问题发生,正如您在上面看到的 ViewModel 不稳定并调用
viewModel.toggle()
或viewModel::toggle
不稳定。
稳定性也适用于 lambda 或回调,您可以在此示例中进行测试
你可以把这个 lambda 保存在里面remember
val onClick = remember {
{ index: Int ->
viewModel.toggleSelection(index)
}
}
Run Code Online (Sandbox Code Playgroud)
并调用ListScreen
为
ListScreen(
people = people,
onItemClick = onClick
)
Run Code Online (Sandbox Code Playgroud)
现在您将看到任何组合MainScreen
仅在文本(标题)中触发,并且ListScreen
不会组合到列表项中。
最后一部分是使 ListScreen 稳定。如果你改变
@Composable
fun ListScreen(
people: List<Person>,
onItemClick: (Int) -> Unit
)
Run Code Online (Sandbox Code Playgroud)
到
@Composable
fun ListScreen(
people: SnapshotStateList<Person>,
onItemClick: (Int) -> Unit
)
Run Code Online (Sandbox Code Playgroud)
你也可以参考这个答案
防止 Jetpack Compose 中的列表更新发生不必要的重组
当 Button 或在您的情况下它可能会触发重组时,不会重组任何内容。
如果您想测试它,还有完整的演示。
class MainActivity : ComponentActivity() {
private val mainViewModel by viewModels<MyViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StabilityTestTheme {
// A surface container using the 'background' color from the theme
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
)
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
7681 次 |
最近记录: |