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

使用Paging3从Room DB加载数据时重复数据引发Key重复崩溃

Paging3 + Room 重复数据导致LazyColumn「Key已被使用」崩溃问题解决

问题场景

使用Paging3从Room数据库加载聊天消息时,出现数据重复,进而触发LazyColumn崩溃,报错信息提示Key was already used。

相关代码

Room查询代码

@Query("SELECT * FROM Message WHERE roomId = :roomId ORDER BY cd DESC limit :pageSize") 
suspend fun getAllMessage(roomId: Long, pageSize: Int): List<Message>

MessagePagingSource实现

class MessagePagingSource(
    private val chatWindowRepository: ChatWindowRepository, 
    private val roomId: Long
) : PagingSource<Int, Message>() {

    override fun getRefreshKey(state: PagingState<Int, Message>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
        return try {
            val pageNumber = params.key ?: 0
            val pageSize = params.loadSize
            val response = chatWindowRepository.getMessages(roomId, pageSize)

            LoadResult.Page(
                data = response,
                prevKey = if (pageNumber > 0) pageNumber - 1 else null,
                nextKey = if (response.isNotEmpty()) pageNumber + 1 else null
            )
        } catch (e: Exception) {
            Log.e("itemSnapshotList", "Error loading data: ${e.message}", e)
            LoadResult.Error(e)
        }
    }
}

ViewModel代码

val messages = Pager( 
    config = PagingConfig(pageSize = 10, prefetchDistance = 10), 
    pagingSourceFactory = { MessagePagingSource(chatWindowRepository, roomId = room.roomId) }
).flow.cachedIn(viewModelScope)

UI代码

val list = vm.messages.collectAsLazyPagingItems()

Log.d("itemSnapshotList", "list size is:${list.itemCount}")

LazyColumn(
    modifier = Modifier
        .padding(it)
        .fillMaxSize(), 
    reverseLayout = true
) {
    items(
        count = list.itemCount,
        key = list.itemKey { message -> message.uuid },
        contentType = list.itemContentType { it.messageType },
        itemContent = {index: Int ->
            val message = list[index]
            Log.d("itemSnapshotList", "uuid is : ${message?.uuid}")
        }
    )
}

崩溃日志

FATAL EXCEPTION: main
Process: com.ainsoft.scoopmessenger, PID: 25528
java.lang.IllegalArgumentException: Key "1-120+1=1697018154509" was already used. If you are using LazyColumn/Row please make sure you provide a unique key for each item.
at androidx.compose.ui.layout.LayoutNodeSubcompositionsState.subcompose(SubcomposeLayout.kt:433)
at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$Scope.subcompose(SubcomposeLayout.kt:807)
at androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScopeImpl.measure-0kLqBqw(LazyLayoutMeasureScope.kt:121)
at androidx.compose.foundation.lazy.LazyListMeasuredItemProvider.getAndMeasure(LazyListMeasuredItemProvider.kt:48)
at androidx.compose.foundation.lazy.LazyListMeasureKt.measureLazyList-50ZNEM8(LazyListMeasure.kt:181)
at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke-0kLqBqw(LazyList.kt:322)
at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke(LazyList.kt:187)
at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3$2$1.invoke-0kLqBqw(LazyLayout.kt:103)
at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3$2$1.invoke(LazyLayout.kt:96)
at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:644)
at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:126)
at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:646)
at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
at androidx.compose.foundation.layout.FillNode.measure-3p2s80s(Size.kt:699)
at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
at androidx.compose.foundation.layout.PaddingValuesModifier.measure-3p2s80s(Padding.kt:455)
at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:116)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1560)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasure$2.invoke(LayoutNodeLayoutDelegate.kt:1556)
at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2303)
at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:473)
at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:230)
at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:113)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1556)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:35)
at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:582)
at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release(LayoutNode.kt:1147)
at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release$default(LayoutNode.kt:1138)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:331)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:466)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtree(MeasureAndLayoutDelegate.kt:546)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtree(MeasureAndLayoutDelegate.kt:554)
at androidx.compose.ui.platform.AndroidComposeView.forceMeasureTheSubtree(AndroidComposeView.android.kt:930)
at androidx.compose.ui.node.Owner.forceMeasureTheSubtree$default(Owner.kt:239)
2023-10-11 15:10:23.929 25528-25528 AndroidRuntime          com.ainsoft.scoopmessenger           E      at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:594)

问题原因

  1. Room查询未实现真正分页:当前查询仅通过limit :pageSize限制返回条数,但缺少offset偏移参数,导致每次加载请求都会返回最新的pageSize条数据,Paging3的列表中出现大量重复的Message项。
  2. PagingSource加载逻辑错误:未根据页码计算数据偏移量,每次加载都请求相同的数据集,分页完全失效,最终导致LazyColumn中出现重复的UUID(作为item的key),触发「Key已被使用」的崩溃。

解决方案

1. 修改Room查询语句,添加分页偏移

更新DAO中的查询方法,增加offset参数,实现基于页码的分页:

@Query("SELECT * FROM Message WHERE roomId = :roomId ORDER BY cd DESC LIMIT :pageSize OFFSET :offset")
suspend fun getAllMessage(roomId: Long, pageSize: Int, offset: Int): List<Message>

2. 调整MessagePagingSource的加载逻辑

在load方法中根据当前页码计算偏移量,确保每次加载不同的分页数据:

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
    return try {
        val pageNumber = params.key ?: 0
        val pageSize = params.loadSize
        // 根据页码计算偏移量:当前页码 * 每页条数
        val offset = pageNumber * pageSize
        val response = chatWindowRepository.getMessages(roomId, pageSize, offset)

        LoadResult.Page(
            data = response,
            prevKey = if (pageNumber > 0) pageNumber - 1 else null,
            nextKey = if (response.isNotEmpty()) pageNumber + 1 else null
        )
    } catch (e: Exception) {
        Log.e("itemSnapshotList", "Error loading data: ${e.message}", e)
        LoadResult.Error(e)
    }
}

3. 可选:验证UUID唯一性(额外保障)

确认Message实体中的uuid字段确实是全局唯一的,避免因UUID本身重复导致的key冲突。可以在插入数据时确保UUID生成逻辑正确,比如使用UUID.randomUUID().toString()生成。

内容的提问来源于stack exchange,提问作者Ain Soft

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 16:02:35