Jetpack Compose LazyColumn移除元素触发IndexOutOfBoundsException崩溃
Jetpack Compose LazyColumn循环移除元素触发IndexOutOfBoundsException崩溃问题
问题场景
- 当列表元素数量≤6且滚动位置处于顶部时,触发崩溃;
- 仅当该LazyColumn所在屏幕为活跃状态时崩溃,非活跃状态下无异常;
- 滚动至底部后执行元素移除操作,循环开始阶段即触发异常。
已确认业务层的增删改操作均添加了索引越界检查,推测异常由Compose重组阶段的LazyColumn内部逻辑触发。
崩溃栈追踪
FATAL EXCEPTION: main Process: com.XXX.YYY.ZZZ, PID: 16225 java.lang.IndexOutOfBoundsException: Index 6, size 6 at androidx.compose.foundation.lazy.layout.MutableIntervalList.checkIndexBounds(IntervalList.kt:177) at androidx.compose.foundation.lazy.layout.MutableIntervalList.get(IntervalList.kt:160) at androidx.compose.foundation.lazy.layout.DefaultLazyLayoutItemsProvider.getKey(LazyLayoutItemProvider.kt:232) at androidx.compose.foundation.lazy.LazyListItemProviderImpl.getKey(Unknown Source:2) at androidx.compose.foundation.lazy.layout.DefaultDelegatingLazyLayoutItemProvider.getKey(LazyLayoutItemProvider.kt:201) at androidx.compose.foundation.lazy.LazyListItemProviderKt$rememberLazyListItemProvider$1$1.getKey(Unknown Source:2) at androidx.compose.foundation.lazy.LazyMeasuredItemProvider.getAndMeasure-ZjPyQlc(LazyMeasuredItemProvider.kt:46) at androidx.compose.foundation.lazy.LazyListMeasureKt.measureLazyList-Hh3qtAg(LazyListMeasure.kt:164) at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke-0kLqBqw(LazyList.kt:299) at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke(LazyList.kt:190) at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$1$2$1.invoke-0kLqBqw(LazyLayout.kt:71) at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$1$2$1.invoke(LazyLayout.kt:69) at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:598) at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:103) at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:635) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:155) at androidx.compose.foundation.layout.SizeModifier.measure-3p2s80s(Size.kt:781) at androidx.compose.ui.node.BackwardsCompatNode.measure-3p2s80s(BackwardsCompatNode.kt:323) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:155) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1090) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1086) at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2200) at androidx.compose.runtime.snapshots.SnapshotStateObserver$observeReads$1$1.invoke(SnapshotStateObserver.kt:234) at androidx.compose.runtime.snapshots.SnapshotStateObserver$observeReads$1$1.invoke(SnapshotStateObserver.kt:230) at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:341) at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source:1) at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:230) at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:120) at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:107) at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1086) at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:36) at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:342) at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release(LayoutNode.kt:1188) at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release$default(LayoutNode.kt:1179) at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:309) at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:434) at androidx.compose.ui.node.MeasureAndLayoutDelegate.access$remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:39) at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:330) at androidx.compose.ui.platform.AndroidComposeView.measureAndLayout(AndroidComposeView.android.kt:805) at androidx.compose.ui.node.Owner.measureAndLayout$default(Owner.kt:221) at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:1035) at android.view.View.draw(View.java:21978) at android.view.View.updateDisplayListIfDirty(View.java:20852) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4552) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4525) at android.view.View.updateDisplayListIfDirty(View.java:20812) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4552) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4525) at android.view.View.updateDisplayListIfDirty(View.java:20812) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4552) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4525) at android.view.View.updateDisplayListIfDirty(View.java:20812) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4552) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4525) at android.view.View.updateDisplayListIfDirty(View.java:20812) at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:581) at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:587) at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:664) at android.view.ViewRootImpl.draw(ViewRootImpl.java:3773) at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3501) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2785) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1751) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7774) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967) at android.view.Choreographer.doCallbacks(Choreographer.java:791) at android.view.Choreographer.doFrame(Choreographer.java:726) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7407) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
使用的Jetpack Compose版本
ext.composeCompilerVersion = 1.4.3 ext { compose_version = composeCompilerVersion //COMPOSE COMPILER VERSION }
相关代码实现
ListView实现
@Composable fun ListViewInternalTest( elements2: List<MyType> ) { val listState = rememberLazyListState() LazyColumn( modifier = Modifier .height(pixelToDp(pixelSize = 792)) .simpleVerticalScrollbar(state = listState), state = listState ) { items( elements2.size ) { index -> Column() { Row( modifier = Modifier .padding(start = pixelToDp(pixelSize = 35)) .height(pixelToDp(pixelSize = 130)), verticalAlignment = Alignment.CenterVertically ) { if(index < elements2.size) { val element = elements2.getOrNull(index) if (element != null) { Text( modifier = Modifier .align(Alignment.Center) .width(pixelToDp(pixelSize = 202)) .alpha(1f), text = "CH " + element.someMember.toString() ) } } } } } } }
列表操作代码
val myList = mutableStateListOf<MyType>() fun insertItemToMyListAtIndex(index: Int, argMyType: MyType){ if(index >= 0 && index <= myList.size) { myList.add(index, argMyType) } } fun updateItemInMyListAtIndex(index: Int, argMyType: MyType) { if(index>= 0 && index< myList .size) { myList[index] = argMyType } } fun removeItemFromMyList(index: Int) { if(index >= 0 && index < myList.size) { myList.removeAt(index) } } fun clearAllitemsInMyList(){ myList.clear() }
问题原因及解决方案
核心原因
- LazyColumn索引同步问题:使用
items(count)方式构建列表时,LazyColumn内部依赖索引跟踪item,当列表元素被快速批量移除时,Compose的测量逻辑可能仍持有旧的索引范围,导致访问超出当前列表大小的索引,触发IndexOutOfBoundsException。 - 旧版本Compose bug:你使用的Compose 1.4.3版本存在已知的Lazy列表状态同步缺陷,后续版本已修复相关问题。
解决方案
1. 升级Compose版本
将Compose版本升级至1.5.x及以上的稳定版,修复旧版本中的Lazy列表状态同步bug。示例配置:
ext.composeCompilerVersion = "1.5.1" ext { compose_version = "1.5.4" }
2. 替换items(count)为items(items)
直接传入列表而非元素数量,让LazyColumn基于元素本身跟踪item,避免索引错位。同时建议为元素指定唯一key,增强状态跟踪准确性:
@Composable fun ListViewInternalTest( elements2: List<MyType> ) { val listState = rememberLazyListState() LazyColumn( modifier = Modifier .height(pixelToDp(pixelSize = 792)) .simpleVerticalScrollbar(state = listState), state = listState ) { // 直接传入列表,并指定元素唯一标识作为key items(elements2, key = { it.someUniqueId }) { element -> Column() { Row( modifier = Modifier .padding(start = pixelToDp(pixelSize = 35)) .height(pixelToDp(pixelSize = 130)), verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier .align(Alignment.Center) .width(pixelToDp(pixelSize = 202)) .alpha(1f), text = "CH " + element.someMember.toString() ) } } } } }
3. 优化批量删除逻辑
如果需要循环删除多个元素,建议从后往前删除避免索引偏移,或使用批量删除方法减少重组次数:
// 从后往前删除,避免索引偏移 fun removeItemsFromRange(startIndex: Int, endIndex: Int) { for (i in endIndex downTo startIndex) { if (i >= 0 && i < myList.size) { myList.removeAt(i) } } } // 批量删除符合条件的元素 fun removeItemsByPredicate(predicate: (MyType) -> Boolean) { myList.removeAll(predicate) }
内容的提问来源于stack exchange,提问作者Mansoor
相关产品推荐
相关产品推荐

