Compose快照状态列表协程搜索时触发ConcurrentModificationException
问题分析与解决
为什么会抛出ConcurrentModificationException?
核心原因有两个:
SnapshotStateList的线程不安全特性:你用mutableStateListOf创建的fullListOfTodos是Compose的快照状态列表,它只能在主线程访问和修改。但你直接在Dispatchers.Default(CPU后台线程)里调用indexOfFirst和访问列表元素,绕过了Compose快照机制的线程校验,导致多线程下状态不一致,触发并发修改检测。- 协程取消时的状态冲突:重复点击刷新会取消旧协程,此时旧协程可能正处于两个阶段:要么在后台线程遍历列表执行
indexOfFirst,要么刚切回主线程准备修改列表。旧协程的取消操作会触发快照状态校验,新协程同时操作同一列表,两者冲突就会抛出异常。
另外,indexOfFirst内部会创建列表迭代器,SnapshotStateList的迭代器会实时检查列表修改状态——迭代过程中如果列表被其他线程/协程修改,就会立刻抛出该异常。
修复方案
1. 严格遵守Compose状态线程规则,优化查找逻辑
把所有对fullListOfTodos的操作移到主线程,同时用Map缓存id到索引的映射,避免每次遍历数千条数据:
// ViewModel中维护id到索引的缓存Map private val todoIdToIndex = mutableMapOf<Long, Int>() // 初始化或更新Todo列表时同步缓存Map fun updateTodoList(newList: List<TodoModel>) { fullListOfTodos.clear() fullListOfTodos.addAll(newList) todoIdToIndex.clear() newList.forEachIndexed { index, todo -> todoIdToIndex[todo.id] = index } } fun fetchFreshTodoItemDetails(idsToRefresh: List<Long>) { fetchJob?.cancel() fetchJob = viewModelScope.launch { when (val todosResponse = apiService.getTodos(idsToRefresh)) { is ApiResult.Success -> { if (todosResponse.value.isEmpty()) return@launch todosResponse.items.forEach { todo -> // 直接从缓存取索引,无需遍历列表 val indexOfTodo = todoIdToIndex[todo.id] ?: return@forEach val currentTodo = fullListOfTodos[indexOfTodo] val updatedTodo = TodoModel(/* 更新需要修改的字段 */) // 主线程安全修改列表 fullListOfTodos[indexOfTodo] = updatedTodo } } // 处理错误情况 else -> {} } } }
2. 协程取消时终止无效操作
在关键步骤检查协程是否活跃,避免取消后继续执行状态修改:
fun fetchFreshTodoItemDetails(idsToRefresh: List<Long>) { fetchJob?.cancel() fetchJob = viewModelScope.launch { when (val todosResponse = apiService.getTodos(idsToRefresh)) { is ApiResult.Success -> { if (todosResponse.value.isEmpty()) return@launch todosResponse.items.forEach { todo -> // 检查协程是否还活跃,取消后直接跳过 ensureActive() val indexOfTodo = fullListOfTodos.indexOfFirst { it.id == todo.id } if (indexOfTodo == -1) return@forEach val currentTodo = fullListOfTodos[indexOfTodo] val updatedTodo = TodoModel(/* 更新字段 */) ensureActive() fullListOfTodos[indexOfTodo] = updatedTodo } } else -> {} } } }
3. 用线程安全容器做中间存储
如果必须在后台处理数据,先把更新后的Todo存在线程安全的容器里,再一次性同步到快照列表:
private val tempTodoMap = ConcurrentHashMap<Long, TodoModel>() fun fetchFreshTodoItemDetails(idsToRefresh: List<Long>) { fetchJob?.cancel() fetchJob = viewModelScope.launch { when (val todosResponse = apiService.getTodos(idsToRefresh)) { is ApiResult.Success -> { if (todosResponse.value.isEmpty()) return@launch // 后台线程安全更新临时Map withContext(Dispatchers.Default) { todosResponse.items.forEach { todo -> tempTodoMap[todo.id] = todo.copy(/* 更新字段 */) } } // 主线程一次性同步到快照列表 fullListOfTodos.forEachIndexed { index, todo -> tempTodoMap[todo.id]?.let { updatedTodo -> fullListOfTodos[index] = updatedTodo } } tempTodoMap.clear() } else -> {} } } }
内容的提问来源于stack exchange,提问作者c_idle
相关产品推荐
相关产品推荐

