You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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"
            )
        }
    }
}

解决方案

问题根源

  1. 状态更新未触发重组:TodoManager.getTodos()直接返回内部mutableListOf的引用,删除操作修改的是同一个列表对象,StateFlow的列表引用未发生变化,Compose无法感知状态更新,导致UI仍使用旧的列表状态。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 01:24:52