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

Paging3结合LazyColumn滚动时,PagingData失效引发锚点位置随机、滚动位置回退问题求助

Paging3结合LazyColumn滚动时,PagingData失效引发锚点位置随机、滚动位置回退问题求助

大家好,我目前在做一个交易列表展示功能,用的是Paging3 + Room组合:所有交易数据从后端拉取后在后台插入本地DB,DB作为唯一数据源。为了避免后台插入数据时频繁触发PagingSource失效,我给失效操作加了2秒防抖,但滚动LazyColumn的过程中还是出现了奇怪的问题——滚动位置会突然回退到之前的旧索引。

排查日志后发现,getRefreshKey()方法里拿到的anchorPosition是随机的,而且LazyColumn会毫无预兆地重新渲染旧索引的Item,比如日志里刚加载到索引29,突然就跳到了索引21。更奇怪的是,只要后台数据同步完成、PagingData不再失效,这个问题就完全消失了。

下面贴一下我的核心代码和日志,想请教大家怎么解决这个问题:

自定义PagingSource实现

class TxnPagingDataSource(
    private val db: MyDatabase,
    roomSqliteQuery: SimpleSQLiteQuery,
    private val observableTables: Array<String> = arrayOf(),
    private val invalidationThreshold: Long = 2000
) : BasePagingSource<TransactionEntity>(db = db) {
    private val countQuery = "SELECT COUNT(*) FROM ( " + roomSqliteQuery.sql + " )"
    private var limitOffsetQuery = "SELECT * FROM ( " + roomSqliteQuery.sql + " ) LIMIT ? OFFSET ?"
    private val invalidated = AtomicBoolean(false)

    init {
        if (observableTables.isNotEmpty()) {
            db.invalidationTracker.addObserver(object : InvalidationTracker.Observer(observableTables) {
                override fun onInvalidated(tables: Set<String>) {
                    db.invalidationTracker.removeObserver(this)
                    if (invalidated.compareAndSet(false, true)) {
                        postInvalidation()
                    }
                }
            })
        }
    }

    private fun postInvalidation() {
        Handler(Looper.getMainLooper()).postDelayed({
            invalidate()
        }, invalidationThreshold)
    }

    override suspend fun getItemCount(): Int {
        val itemCount = db.getTransactionDao().getLongValue(SimpleSQLiteQuery(countQuery))?.toInt() ?: 0
        return itemCount
    }

    override suspend fun getItems(offset: Int, limit: Int): List<TransactionEntity> {
        val transactionEntities = db.getTransactionCoreDao()
            .transactionsRawQuery(SimpleSQLiteQuery(limitOffsetQuery, arrayOf(limit, offset)))
        return transactionEntities
    }

    override fun getRefreshKey(state: PagingState<Int, TransactionEntity>): Int? {
        val closestItem = state.anchorPosition?.let { state.closestItemToPosition(it)?.transactionId }
        val refreshKey = super.getRefreshKey(state)
        Logger.logd { "RenderDebug[TxnScrollDebug]: getRefreshKey with anchorPosition:${state.anchorPosition}, closestItemId: $closestItem, refreshKey: $refreshKey" }
        return refreshKey
    }
}

LazyColumn实现

@Composable
fun TransactionListScreen(viewModel: MyTransactionViewModel) {
    val txnPagedData = viewModel.transactionPagedData.collectAsLazyPagingItems()
    val actionClick: (Map<MetaData, Any?>) -> Unit = remember { { } }
    val lazyListState = rememberLazyListState()

    LazyColumn(
        state = lazyListState,
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.White)
    ) {
        items(
            count = txnPagedData.itemCount,
            key = txnPagedData.itemKey { it.transactionId }) { index: Int ->
            val data = txnPagedData[index]
            val txnId = data.transactionId
            Logger.logd { "RenderDebug[TxnScrollDebug] loading index on LazyColumn: $index, for txnId: ${txnId}" }
            TransactionHistoryListItem(
                title = data.titleDisplayText,
                name = data.payeeNameDisplayText,
                time = data.displayTimeStampText,
                amount = data.displayAmount,
                debitCreditText = data.debitCreditInfoDisplayText,
                imageUrl = "https://www.google.com"
            )
        }
    }
}

问题日志片段

RenderDebug[TxnScrollDebug] loading index on LazyColumn: 26
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 27
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 24
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 25
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 23
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 22
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 28
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 29
RenderDebug[TxnScrollDebug]: LOAD_INFO, prevKey: 36, nextKey: 66 , itemsBefore:36 , itemAfter: 1169
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 21
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 26
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 27
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 24
RenderDebug[TxnScrollDebug] loading index on LazyColumn: 25

已尝试的优化

  • 完全参考Room官方自动生成的PagingSource逻辑实现了load和getRefreshKey,没有引入额外依赖
  • 给PagingSource的失效操作加了2秒防抖,减少了频繁失效的次数

想请教大家两个问题:

  1. 这种锚点位置随机、滚动回退的问题,到底是PagingData内部机制导致的,还是LazyColumn的渲染逻辑问题?
  2. 有没有办法在PagingSource失效刷新时,稳定住当前的滚动位置,避免回退?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 09:34:31