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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 16:42:01