Kotlin Multiplatform Compose应用删除Todo项触发IndexOutOfBoundsException
问题:Jetpack Compose Kotlin Multiplatform待办应用删除项触发IndexOutOfBoundsException
使用Jetpack Compose构建Kotlin Multiplatform待办应用,支持添加和删除待办项,添加功能正常,但删除项时会触发IndexOutOfBoundsException,推测是UI尝试访问已被移除的项索引,更新依赖后问题仍存在。
相关代码片段
TodoViewModel.kt
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class TodoViewModel { private val _todoList = MutableStateFlow<List<Todo>>(emptyList()) val todos: StateFlow<List<Todo>> get() = _todoList init { loadTodos() } private fun loadTodos() { _todoList.value = TodoManager.getTodos() } fun addTodo(title: String) { TodoManager.addTodo(title) loadTodos() } fun removeTodo(id: Int) { TodoManager.removeTodo(id) loadTodos() } }
TodoManager.kt
import java.time.Instant import java.util.* object TodoManager { private var todoList = mutableListOf<Todo>() fun addTodo(title: String) { todoList.add(Todo(System.currentTimeMillis().toInt(), title, Date.from(Instant.now()))) } fun removeTodo(id: Int) { todoList.removeIf { it.id == id } } fun getTodos(): List<Todo> { return todoList } }
Todo.kt
import java.util.Date data class Todo( var id: Int, var title: String, var createdAt: Date )
MainUI.kt
@Composable fun TodoListPage(todoViewModel: TodoViewModel = TodoViewModel()) { val todos by todoViewModel.todos.collectAsState() var inputText by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxHeight().padding(8.dp)) { Row( modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly ) { OutlinedTextField( modifier = Modifier.weight(1f), value = inputText, onValueChange = { inputText = it } ) Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { if (inputText.isNotBlank()) { todoViewModel.addTodo(inputText) inputText = "" } }) { Text(text = "Add Todo") } } LazyColumn { items(todos) { item -> todoItem(item, removeTodo = { todoViewModel.removeTodo(item.id) }) } } } } @Composable fun todoItem(todo: Todo, removeTodo: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() .padding(8.dp) .clip(RoundedCornerShape(16.dp)) .background(MaterialTheme.colors.primary) .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { Text( text = SimpleDateFormat("HH:mm:ss, dd-MMM-yyyy", Locale.ENGLISH).format(todo.createdAt), color = Color.LightGray, fontSize = 10.sp, ) Text( text = todo.title, fontSize = 20.sp, color = Color.White ) } IconButton(onClick = { removeTodo() }) { Icon( Icons.Rounded.Delete, tint = Color.White, contentDescription = "Delete" ) } } }
解决方案
问题根源
- 状态更新未触发重组:
TodoManager.getTodos()直接返回内部mutableListOf的引用,删除操作修改的是同一个列表对象,StateFlow的列表引用未发生变化,Compose无法感知状态更新,导致UI仍使用旧的列表状态。 - LazyColumn依赖索引定位项:默认的
items(todos)使用列表索引作为项的标识,删除项后列表长度变化,但UI可能仍尝试访问旧索引,引发IndexOutOfBoundsException。
修复步骤
1. 让TodoManager返回不可变列表副本
修改TodoManager.getTodos(),返回列表的不可变副本,确保每次状态更新都是新的对象引用:
fun getTodos(): List<Todo> { return todoList.toList() // 返回不可变副本,而非原MutableList引用 }
2. 为LazyColumn项添加唯一Key
修改LazyColumn的items方法,使用Todo的id作为唯一标识,避免依赖索引定位项:
LazyColumn { items(todos, key = { it.id }) { item -> todoItem(item, removeTodo = { todoViewModel.removeTodo(item.id) }) } }
3. 简化ViewModel状态更新逻辑(可选优化)
移除冗余的loadTodos方法,直接在操作后更新StateFlow,代码更简洁:
class TodoViewModel { private val _todoList = MutableStateFlow<List<Todo>>(emptyList()) val todos: StateFlow<List<Todo>> get() = _todoList init { _todoList.value = TodoManager.getTodos() } fun addTodo(title: String) { TodoManager.addTodo(title) _todoList.value = TodoManager.getTodos() } fun removeTodo(id: Int) { TodoManager.removeTodo(id) _todoList.value = TodoManager.getTodos() } }
修复说明
- 返回不可变副本:确保StateFlow每次更新都生成新的List对象,触发Compose的重组机制,让UI同步最新的列表状态。
- 使用唯一Key:LazyColumn通过Todo的
id识别每个项,即使列表项顺序变化或被删除,也能正确定位和重组对应的UI元素,彻底避免索引越界问题。
内容的提问来源于stack exchange,提问作者Hammad Latif
相关产品推荐
相关产品推荐

