JetPack Compose嵌套LazyRow分页UI闪烁问题及隔离方案咨询
问题:嵌套LazyRow的LazyColumn分页刷新导致全列表UI闪烁
在JetPack Compose中实现了嵌套LazyRow的LazyColumn列表,每个LazyRow通过Flow<PagingData<UiObj>>展示分页数据。核心代码结构如下:
父列表LazyColumn实现
LazyColumn() { items( items = showcaseRefList, key = { it.urn }, contentType = { "showcaseRow" } ) { val showcaseRowViewModel: ShowcaseRowViewModel = koinViewModel(key = it.urn) { parametersOf( it.urn, it.type.nameType ) } ShowcaseRow( uiModelShowcasePageItem = it, itemFlow = showcaseRowViewModel.pagerFlow, onShowcaseItemClick = { } ) } }
分页数据获取逻辑
通过RemoteMediator结合Room的PagingSource实现分页:
@OptIn(ExperimentalPagingApi::class) val pagerFlow = Pager( config = PagingConfig( pageSize = 20, initialLoadSize = 1, prefetchDistance = 5 ), remoteMediator = remoteMediator, pagingSourceFactory = pagingSourceFactory ).flow .cachedIn(viewModelScope)
Room Dao查询
@Query("select * from DbModelShowcaseBaseItem where parentUrn = :urn order by `order`") abstract fun getShowcaseItemsByUrnPagingSource(urn: String): PagingSource<Int, DbModelShowcaseItem>
问题现象:滚动某一行触发RemoteMediator拉取数据并存入Room时,所有针对该表的PagingSource都会触发invalidate回调,导致所有LazyRow出现UI闪烁。已尝试distinctUntilChanged无效,因为Room每次都会生成新的PagingSource对象。
解决方案
方法一:自定义PagingSource,精准控制无效化触发时机
Room默认会在整个表发生增删改时,触发所有关联该表的PagingSource无效化。我们可以自定义PagingSource,只在当前行对应的parentUrn数据变化时才触发无效化。
1. 扩展Dao方法
添加分页查询和数据变化检查的方法:
@Dao interface ShowcaseDao { // 分页查询数据(替代自动生成的PagingSource) @Query("select * from DbModelShowcaseBaseItem where parentUrn = :urn order by `order` limit :limit offset :offset") abstract suspend fun getShowcaseItemsByUrnPaginated(urn: String, offset: Int, limit: Int): List<DbModelShowcaseItem> // 检查指定parentUrn的数据是否有更新 @Query("select count(*) from DbModelShowcaseBaseItem where parentUrn = :urn and lastUpdated > :lastCheckTime") abstract suspend fun hasChangesForParentUrn(urn: String, lastCheckTime: Long): Boolean }
注意:需要给
DbModelShowcaseBaseItem添加lastUpdated字段,用于记录数据的更新时间。
2. 自定义PagingSource
class ShowcaseItemPagingSource( private val dao: ShowcaseDao, private val parentUrn: String, private val viewModelScope: CoroutineScope ) : PagingSource<Int, DbModelShowcaseItem>() { private var lastCheckTime = System.currentTimeMillis() private val observer = object : InvalidationTracker.Observer("DbModelShowcaseBaseItem") { override fun onInvalidated(tables: MutableSet<String>) { // 仅当当前parentUrn的数据有更新时,才触发PagingSource无效化 viewModelScope.launch { val hasChanges = dao.hasChangesForParentUrn(parentUrn, lastCheckTime) if (hasChanges) { lastCheckTime = System.currentTimeMillis() invalidate() } } } } init { dao.invalidationTracker.addObserver(observer) } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DbModelShowcaseItem> { return try { val page = params.key ?: 0 val offset = page * 20 val items = dao.getShowcaseItemsByUrnPaginated(parentUrn, offset, 20) val nextKey = if (items.size < 20) null else page + 1 LoadResult.Page( data = items, prevKey = if (page == 0) null else page - 1, nextKey = nextKey ) } catch (e: Exception) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, DbModelShowcaseItem>): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } override fun invalidate() { dao.invalidationTracker.removeObserver(observer) super.invalidate() } }
3. 替换Pager的pagingSourceFactory
在ViewModel中使用自定义的PagingSource:
val pagingSourceFactory = { ShowcaseItemPagingSource(dao, parentUrn, viewModelScope) }
方法二:基于数据内容过滤重复的PagingData
通过判断PagingData中的实际内容是否变化,避免无意义的UI刷新。利用mapLatest提取数据快照的特征值,结合distinctUntilChangedBy过滤重复发射。
@OptIn(ExperimentalPagingApi::class) val pagerFlow = Pager( config = PagingConfig( pageSize = 20, initialLoadSize = 1, prefetchDistance = 5 ), remoteMediator = remoteMediator, pagingSourceFactory = pagingSourceFactory ).flow .cachedIn(viewModelScope) .mapLatest { pagingData -> // 生成数据快照并计算特征值(这里用数据列表的hashCode) val snapshot = pagingData.snapshot() pagingData to snapshot.items.hashCode() } .distinctUntilChangedBy { it.second } // 仅当特征值变化时才继续传递 .map { it.first } // 还原为PagingData
该方法无需修改数据库结构,适合快速解决问题,但频繁生成快照可能有轻微性能损耗,可根据实际场景调整特征值的计算方式(比如用数据的唯一标识集合的hash)。
内容的提问来源于stack exchange,提问作者Paul Yurkin
相关产品推荐
相关产品推荐

