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

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

问题原因及解决方案

核心原因

  1. LazyColumn索引同步问题:使用items(count)方式构建列表时,LazyColumn内部依赖索引跟踪item,当列表元素被快速批量移除时,Compose的测量逻辑可能仍持有旧的索引范围,导致访问超出当前列表大小的索引,触发IndexOutOfBoundsException。
  2. 旧版本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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 04:45:54