【问题标题】:How can I correctly use custom PagingSource with PagingDataAdapter, on local data?如何在本地数据上正确使用带有 PagingDataAdapter 的自定义 PagingSource?
【发布时间】:2022-07-13 17:21:34
【问题描述】:

问题

我有本地生成的数据,需要在 RecyclerView 中显示。我尝试使用自定义PagingSourcePagingDataAdapter 来减少内存中的数据量,但是当我使数据无效时会出现视觉效果,例如,如果我插入或删除一项:

  • 当可见项属于 2 个不同的“页面”时,许多未更改的项会像被修改过一样闪烁
  • 如果所有可见项都属于同一页面,则一切正常。只有插入/删除的项目会显示动画。

我使用 Google 文档 (PagingSample) 引用的示例应用程序来测试这个概念。带有 Room 的原始版本不显示人工制品,但带有自定义 PagingSource 的修改版本可以。

Room 生成和使用的代码过于复杂,看不出任何可以解释问题的差异。

我的数据必须是本地生成的,我不能使用 Room 作为解决方法来显示它们。

我的问题

如何为我的本地数据正确定义 PagingSource,并将其与 PagingDataAdapter 一起使用而不会出现视觉故障?

(可选)我如何知道数据何时被丢弃(这样我也可以丢弃本地数据)?

代码摘录和详细信息

完整的示例项目托管在这里:https://github.com/blueglyph/PagingSampleModified

这是数据:

    private val _data = ArrayMap<Int, Cheese>()
    val data = MutableLiveData <Map<Int, Cheese>>(_data)
    val sortedData = data.map { data -> data.values.sortedBy { it.name.lowercase() } }

PagingSource。我正在使用 key = item 位置。我试过用 key = 页码,每页包含 30 个项目(10 个可见),但它并没有改变任何东西。

    private class CheeseDataSource(val dao: CheeseDaoLocal, val pageSize: Int): PagingSource<Int, Cheese>() {
        fun max(a: Int, b: Int): Int = if (a > b) a else b

        override fun getRefreshKey(state: PagingState<Int, Cheese>): Int? {
            val lastPos = dao.count() - 1
            val key = state.anchorPosition?.let { anchorPosition ->
                val anchorPage = state.closestPageToPosition(anchorPosition)
                anchorPage?.prevKey?.plus(pageSize)?.coerceAtMost(lastPos) ?: anchorPage?.nextKey?.minus(pageSize)?.coerceAtLeast(0)
            }
            return key
        }

        override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Cheese> {
            val pageNumber = params.key ?: 0
            val count = dao.count()
            val data = dao.allCheesesOrdName().drop(pageNumber).take(pageSize)
            return LoadResult.Page(
                data = data,
                prevKey = if (pageNumber > 0) max(0, pageNumber - pageSize) else null,
                nextKey = if (pageNumber + pageSize < count) pageNumber + pageSize else null
            )
        }
    }

PagingData 上的Flow 是在视图模型中创建的:

    val pageSize = 30
    var dataSource: PagingSource<Int, Cheese>? = null
    val allCheeses: Flow<PagingData<CheeseListItem>> = Pager(
        config = PagingConfig(
            pageSize = pageSize,
            enablePlaceholders = false,
            maxSize = 90
        )
    ) {
        dataSource = dao.getDataSource(pageSize)
        dataSource!!
    }.flow
        .map { pagingData -> pagingData.map { cheese -> CheeseListItem.Item(cheese) } }

dao.getDataSource(pageSize) 返回上面显示的CheeseDataSource

在活动中,数据页面被收集并提交:

        lifecycleScope.launch {
            viewModel.allCheeses.collectLatest { adapter.submitData(it) }
        }

当数据被修改时,观察者触发失效:

        dao.sortedData.observeForever {
            dataSource?.invalidate()
        }

页面的滚动和加载都很好,唯一的问题是使用invalidate和同时显示2个页面的项目时。

适配器很经典:

class CheeseAdapter : PagingDataAdapter<CheeseListItem, CheeseViewHolder>(diffCallback) {
...
    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<CheeseListItem>() {
            override fun areItemsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
                return if (oldItem is CheeseListItem.Item && newItem is CheeseListItem.Item) {
                    oldItem.cheese.id == newItem.cheese.id
                } else if (oldItem is CheeseListItem.Separator && newItem is CheeseListItem.Separator) {
                    oldItem.name == newItem.name
                } else {
                    oldItem == newItem
                }
            }
            override fun areContentsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
                return oldItem == newItem
            }
        }
...

我尝试过的事情(以及许多其他事情)

  • 使用 LiveData 代替 Flow
  • 使用/删除 Flow 上的缓存
  • 直接在插入/删除函数中移除观察者和无效化,让代码更直接
  • 使用 key = 页码 (0, 1, 2, ...) 代替 key = position,每个页面包含 pageSize=30 个项目

此时,我不确定 paging-3 是否用于自定义数据。我正在观察一个简单的插入/删除操作,例如适配器中的 2000-4000 比较操作,重新加载 3 页数据,......直接在我的数据上使用 ListAdapter 并手动执行加载/卸载似乎是更好的选择。

【问题讨论】:

    标签: android kotlin android-paging-3


    【解决方案1】:

    2022 年 7 月更新

    page-3 存在一些问题。最引人注目的是

    • 在使用initialKey 的初始位置时无法正确加载数据集(请参阅initial_key 分支)
    • 无法正确跳转到另一个位置,使用scrollToPositionWithOffset 或类似功能(参见test_scroll 分支)

    getRefreshKey 函数以奇怪的(有时为负)anchorPosition 值调用,并且 load 被多次调用以加载不必要的数据 - 有时从末尾或从开头迭代地加载每一页。

    => 简而言之,它只能在需要先显示顶部项目,并且不需要跳转到特定位置时使用(仅当用户手动滚动浏览其他项目时)列表)。修复正在进行中,但可能需要时间;它仍然只是 alpha 版,并且针对一组特定的库版本/SDK 进行编译。

    您会在上述分支中找到另一个更简单的 getRefreshKeyload 算法。

    更新结束


    我终于找到了一个可能的解决方案,虽然我不确定如果 Paging-3 库更新它会起作用。我还不知道上面解释的行为是由于 Paging-3 组件中的错误/限制,还是只是参考文档中的错误解释,或者我完全错过了。

    我。故障问题的解决方法。

    1. PagingConfig 中,我们必须拥有enablePlaceholders = true,否则它将无法正常工作。使用false,我在向上/向下滚动时观察每个load 操作上的滚动条跳跃,并且在末尾插入项目将使所有项目在显示上出现故障,然后列表将一直跳到顶部。

    2. getRefreshKeyload 中的逻辑(如 Google 的指南和参考文档中所示)是幼稚的,不适用于自定义数据。我不得不将它们修改如下(修改已在 github 示例中推送):

            override fun getRefreshKey(state: PagingState<Int, Cheese>): Int? = state.anchorPosition
    
            override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Cheese> {
                data class Args(
                    var start: Int = 0, var size: Int = 0, var prevKey: Int? = null, var nextKey: Int? = null,
                    var itemsBefore: Int = UNDEF, var itemsAfter: Int = UNDEF
                )
                val pos = params.key ?: 0
                val args = Args()
                when (params) {
                    is LoadParams.Append -> {
                        args.start = pos
                        args.prevKey = params.key
                        //args.nextKey = if (args.start < count) min(args.start + params.loadSize, count) else null
                        args.nextKey = if (args.start + params.loadSize < count) args.start + params.loadSize else null
                    }
                    is LoadParams.Prepend -> {
                        args.start = max(pos - pageSize, 0)
                        args.prevKey = if (args.start > 0) args.start else null
                        args.nextKey = params.key
                    }
                    is LoadParams.Refresh -> {
                        args.start = max((pos - params.loadSize/2)/pageSize*pageSize, 0)
                        args.prevKey = if (args.start > 0) args.start else null
                        args.nextKey = if (args.start + params.loadSize < count) min(args.start + params.loadSize, count - 1) else null
                    }
                }
                args.size = min(params.loadSize, count - args.start)
                if (params is LoadParams.Refresh) {
                    args.itemsBefore = args.start
                    args.itemsAfter = count - args.size - args.start
                }
                val source = dao.allCheesesOrdName()
                val data = source.drop(args.start).take(args.size)
                if (params.key == null && data.count() == 0) {
                    return LoadResult.Error(Exception("Empty"))
                }
                val result = LoadResult.Page(
                    data = data,
                    prevKey = args.prevKey,
                    nextKey = args.nextKey,
                    itemsBefore = args.itemsBefore,
                    itemsAfter = args.itemsAfter
                )
                return result
            }
    

    我必须从 Room 生成的 PagingSource 的行为中推断出这一点,方法是在 DAO 代码和视图模型之间插入一个包装器以观察 paramsLoadResult 值。

    注意事项

    • LoadParam.Append 中的注释行模仿了 Room 生成的代码行为,在加载最后一页时略有错误。它使Pager在最后加载一个空页面,这不是一个严重的问题,但会触发整个链条中不必要的操作。
    • 我不确定Refresh 的情况,很难从 Room 的代码行为中推断出任何逻辑。在这里,我将params.key 位置放在加载范围的中间(params.loadSize 项)。在最坏的情况下,它会在第二次加载操作中附加数据。

    二。预测丢弃的数据

    这应该可以通过将params 赋予load 函数来实现。在简化的启发式中(范围必须最小/最大才能获得实际索引):

    LoadParams.Append  -> loads [key .. key+loadSize[, so discard [key-maxSize..key-maxSize+loadSize[
    LoadParams.Prepend -> loads [key-loadSize .. key[, do discard [key-loadSize+maxSize..key+maxSize[
    LoadParams.Refresh -> discard what is not reloaded
    

    上面代码中的Args.start .. Args.start + Args.size可以作为被保留的范围,从那里很容易推断出什么被丢弃了。

    【讨论】:

    • 嘿,你能看看这个issue
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-12-21
    • 2017-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    相关资源
    最近更新 更多