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秒防抖,减少了频繁失效的次数
想请教大家两个问题:
- 这种锚点位置随机、滚动回退的问题,到底是PagingData内部机制导致的,还是LazyColumn的渲染逻辑问题?
- 有没有办法在PagingSource失效刷新时,稳定住当前的滚动位置,避免回退?
内容来源于stack exchange

