Jetpack Compose排序列表问题:列表项消失后重现
问题:Jetpack Compose LazyColumn列表项移动时底部项消失后重现
我实现了一个按distance字段排序的LazyColumn列表,当某个列表项的distance更新导致位置变动时,列表底部的若干项会短暂消失,经过一两次更新后又重新出现。
我的排序逻辑是:先移除目标列表项,再通过前后线性搜索找到新位置插入,因为我认为项的移动范围不会超过一两行。
更新操作仅在单个线程执行,但我不确定Composable是否线程安全?或者组合列表时是否需要同步操作?
相关代码实现
ViewModel代码
class TodoViewModel : ViewModel() { private val _todos = emptyList<Todo>().toMutableStateList() val todos: List<Todo> get() = _todos fun changeTitle(id: Int, title: String) { var index = _todos.indexOfFirst { todo -> todo.id == id } if (index == -1) return _todos[index] = _todos[index].copy(title = title) } fun delete(index: Int) { _todos.removeAt(index) } fun changeDistance(id: Int, delta: Int) { var index = _todos.indexOfFirst { todo -> todo.id == id } if (index == -1) return val distance = _todos[index].distance + delta val oldDistance = _todos[index].distance Log.i("xx", "Update $id at $index from $oldDistance to $distance") val newTodo = _todos[index].copy(distance = distance) if ((index == 0 || distance >= _todos[index-1].distance) && (index == _todos.size-1 || distance <= _todos[index+1].distance)) { _todos[index] = newTodo return } Log.i("xx", "Move $id from $index") delete(index) while (index > 0 && distance < todos[index-1].distance) { index-- } while (index < todos.size && distance > todos[index].distance) { index++ } Log.i("xx", "Move $id to $index") _todos.add(index, newTodo) } fun insert(todo: Todo) { val index = todos.indexOfFirst { t -> t.id == todo.id } if (index != -1) { _todos[index] = todo return } var newIndex = todos.binarySearch {t -> t.distance - todo.distance } if (newIndex < 0) newIndex = (-newIndex - 1) _todos.add(newIndex, todo) } }
Main Activity代码
private val todoViewModel = TodoViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { TodoScreen(todoViewModel = todoViewModel) } } var nextId = 0 val thread = Thread { for (i in 0..10) { todoViewModel.insert(Todo(nextId, "new $nextId", Random.nextInt(25, 50))) nextId++ } for (i in 0..100) { Thread.sleep(500) val functionNum = Random.nextInt(0, 100) val index = Random.nextInt(0, todoViewModel.todos.size) val id = todoViewModel.todos[index].id todoViewModel.changeDistance(id, Random.nextInt(-10, 10)) } } thread.start() } } @Composable fun TodoScreen( modifier: Modifier = Modifier, todoViewModel: TodoViewModel = viewModel() ) { Surface(color = MaterialTheme.colorScheme.background) { TodoList( modifier = modifier.fillMaxSize(), list = todoViewModel.todos ) } } @Composable fun TodoList( list: List<Todo>, modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(items = list, key = { todo -> todo.id }) { todo -> TodoItem(todo = todo, modifier = Modifier.fillMaxWidth() ) } } } @Composable fun TodoItem(todo: Todo, modifier: Modifier ) { Card( modifier = modifier, elevation = CardDefaults.cardElevation(8.dp, 8.dp, 8.dp, 8.dp, 8.dp, 8.dp) ) { Row(modifier = Modifier.padding(8.dp)) { Text(text = todo.title) Spacer(modifier = Modifier.weight(1f)) Text(text = todo.distance.toString()) } } }
问题原因与解决方案
核心问题分析
- 非主线程更新状态容器:
MutableStateList是Compose的状态容器,仅支持主线程修改。你在后台线程直接调用ViewModel方法修改列表,会导致状态更新与UI重组不同步,引发项消失、重复等异常。 - 重排序时索引计算错误:删除项后继续使用原
index变量搜索新位置,此时列表长度已减少1,索引偏移会导致插入位置错误,进而干扰UI渲染逻辑。
修复方案
1. 强制在主线程更新状态
使用viewModelScope(默认绑定主线程)执行所有状态修改操作,避免后台线程直接操作MutableStateList。
2. 修复重排序的索引逻辑
删除项后,重新初始化搜索起始索引,基于更新后的列表计算插入位置。
修改后的代码
ViewModel代码
class TodoViewModel : ViewModel() { private val _todos = emptyList<Todo>().toMutableStateList() val todos: List<Todo> get() = _todos fun changeTitle(id: Int, title: String) { val index = _todos.indexOfFirst { it.id == id } if (index == -1) return _todos[index] = _todos[index].copy(title = title) } fun delete(index: Int) { if (index in _todos.indices) { _todos.removeAt(index) } } fun changeDistance(id: Int, delta: Int) { val index = _todos.indexOfFirst { it.id == id } if (index == -1) return val oldTodo = _todos[index] val newDistance = oldTodo.distance + delta val newTodo = oldTodo.copy(distance = newDistance) // 检查是否无需移动位置 val needMove = !( (index == 0 || newDistance >= _todos[index-1].distance) && (index == _todos.lastIndex || newDistance <= _todos[index+1].distance) ) if (!needMove) { _todos[index] = newTodo return } // 删除旧项后,基于新列表重新计算插入位置 _todos.removeAt(index) var newIndex = 0 while (newIndex < _todos.size && newDistance > _todos[newIndex].distance) { newIndex++ } _todos.add(newIndex, newTodo) } fun insert(todo: Todo) { val existingIndex = _todos.indexOfFirst { it.id == todo.id } if (existingIndex != -1) { _todos[existingIndex] = todo return } val newIndex = _todos.binarySearch { it.distance - todo.distance }.let { if (it < 0) -(it + 1) else it } _todos.add(newIndex, todo) } }
Main Activity代码
class MainActivity : ComponentActivity() { private val todoViewModel by viewModels<TodoViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { TodoScreen(todoViewModel = todoViewModel) } } var nextId = 0 // 使用viewModelScope确保状态更新在主线程执行 todoViewModel.viewModelScope.launch { for (i in 0..10) { todoViewModel.insert(Todo(nextId, "new $nextId", Random.nextInt(25, 50))) nextId++ delay(100) // 避免批量插入过快导致UI卡顿 } for (i in 0..100) { delay(500) val todos = todoViewModel.todos if (todos.isEmpty()) continue val index = Random.nextInt(0, todos.size) val id = todos[index].id todoViewModel.changeDistance(id, Random.nextInt(-10, 10)) } } } } @Composable fun TodoScreen( modifier: Modifier = Modifier, todoViewModel: TodoViewModel ) { Surface(color = MaterialTheme.colorScheme.background) { TodoList( modifier = modifier.fillMaxSize(), list = todoViewModel.todos ) } } @Composable fun TodoList( list: List<Todo>, modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(8.dp) ) { items(items = list, key = { todo -> todo.id }) { todo -> TodoItem(todo = todo, modifier = Modifier.fillMaxWidth()) } } } @Composable fun TodoItem(todo: Todo, modifier: Modifier) { Card( modifier = modifier, elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) ) { Row(modifier = Modifier.padding(8.dp)) { Text(text = todo.title) Spacer(modifier = Modifier.weight(1f)) Text(text = todo.distance.toString()) } } } data class Todo(val id: Int, val title: String, val distance: Int)
内容的提问来源于stack exchange,提问作者user998303
相关产品推荐
相关产品推荐

