首页 > 解决方案 > 无效的自定义 PageKeyedDataSource 使回收器视图跳转

问题描述

我正在尝试使用自定义 PageKeyedDataSource 实现一个 android 分页库,该数据源将从数据库中查询数据并在该页面上随机插入广告。

我实现了分页,但是每当我滚动到第二页并使数据源无效时,回收器视图就会跳回到第二页的末尾。

这是什么原因?

数据源:

    class ColorsDataSource(
    private val colorsRepository: ColorsRepository
) : PageKeyedDataSource<Int, ColorEntity>() {

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, ColorEntity>
    ) {
        Timber.i("loadInitial()  offset 0 params.requestedLoadSize $params.requestedLoadSize")
        val resultFromDB = colorsRepository.getColors(0, params.requestedLoadSize)
        // TODO insert Ads here
        callback.onResult(resultFromDB, null, 1)
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ColorEntity>) {
        val offset = params.key * params.requestedLoadSize
        Timber.i("loadAfter()    offset $offset params.requestedLoadSize $params.requestedLoadSize")
        val resultFromDB = colorsRepository.getColors(
            offset,
            params.requestedLoadSize
        )
        // TODO insert Ads here
        callback.onResult(resultFromDB, params.key + 1)
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, ColorEntity>) {
        // No- Op
    }
}

边界回调

class ColorsBoundaryCallback(
    private val colorsRepository: ColorsRepository,
    ioExecutor: Executor,
    private val invalidate: () -> Unit
) : PagedList.BoundaryCallback<ColorEntity>() {

    private val helper = PagingRequestHelper(ioExecutor)

    /**
     * Database returned 0 items. We should query the backend for more items.
     */
    @MainThread
    override fun onZeroItemsLoaded() {
        helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) { pagingRequestHelperCallback ->
            Timber.i("onZeroItemsLoaded() ")
            colorsRepository.colorsApiService.getColorsByCall(
                ColorsRepository.getQueryParams(
                    1,
                    ColorViewModel.PAGE_SIZE
                )
            ).enqueue(object : Callback<List<ColorsModel?>?> {
                override fun onFailure(call: Call<List<ColorsModel?>?>, t: Throwable) {
                    handleFailure(t, pagingRequestHelperCallback)
                }

                override fun onResponse(
                    call: Call<List<ColorsModel?>?>,
                    response: Response<List<ColorsModel?>?>
                ) {
                    handleSuccess(response, pagingRequestHelperCallback)
                }
            })
        }
    }

    private fun handleSuccess(
        response: Response<List<ColorsModel?>?>,
        pagingRequestHelperCallback: PagingRequestHelper.Request.Callback
    ) {
        colorsRepository.saveColorsIntoDb(response.body())
        invalidate.invoke()
        Timber.i("onZeroItemsLoaded() with listOfColors")
        pagingRequestHelperCallback.recordSuccess()
    }

    /**
     * User reached to the end of the list.
     */
    @MainThread
    override fun onItemAtEndLoaded(itemAtEnd: ColorEntity) {
        Timber.i("onItemAtEndLoaded() ")
        helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { pagingRequestHelperCallback ->
            val nextPage = itemAtEnd.nextPage?.toInt() ?: 0
            colorsRepository.colorsApiService.getColorsByCall(
                ColorsRepository.getQueryParams(
                    nextPage,
                    ColorViewModel.PAGE_SIZE
                )
            ).enqueue(object : Callback<List<ColorsModel?>?> {
                override fun onFailure(call: Call<List<ColorsModel?>?>, t: Throwable) {
                    handleFailure(t, pagingRequestHelperCallback)
                }

                override fun onResponse(
                    call: Call<List<ColorsModel?>?>,
                    response: Response<List<ColorsModel?>?>
                ) {
                    handleSuccess(response, pagingRequestHelperCallback)
                }

            })
        }
    }

    private fun handleFailure(
        t: Throwable,
        pagingRequestHelperCallback: PagingRequestHelper.Request.Callback
    ) {
        Timber.e(t)
        pagingRequestHelperCallback.recordFailure(t)
    }
}

适配器的 DiffUtil

class DiffUtilCallBack : DiffUtil.ItemCallback<ColorEntity>() {
        override fun areItemsTheSame(oldItem: ColorEntity, newItem: ColorEntity): Boolean {
            return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: ColorEntity, newItem: ColorEntity): Boolean {
            return oldItem.hexString == newItem.hexString
                    && oldItem.name == newItem.name
                    && oldItem.colorId == newItem.colorId
        }
    }

视图模型

class ColorViewModel(private val repository: ColorsRepository) : ViewModel() {

    fun getColors(): LiveData<PagedList<ColorEntity>> = postsLiveData

    private var postsLiveData: LiveData<PagedList<ColorEntity>>
    lateinit var dataSourceFactory: DataSource.Factory<Int, ColorEntity>
    lateinit var dataSource: ColorsDataSource

    init {
        val config = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setEnablePlaceholders(false)
            .build()

        val builder = initializedPagedListBuilder(config)
        val contentBoundaryCallBack =
            ColorsBoundaryCallback(repository, Executors.newSingleThreadExecutor()) {
                invalidate()
            }
        builder.setBoundaryCallback(contentBoundaryCallBack)
        postsLiveData = builder.build()
    }

    private fun initializedPagedListBuilder(config: PagedList.Config):
            LivePagedListBuilder<Int, ColorEntity> {

        dataSourceFactory = object : DataSource.Factory<Int, ColorEntity>() {
            override fun create(): DataSource<Int, ColorEntity> {
                dataSource =  ColorsDataSource(repository)
                return dataSource
            }
        }
        return LivePagedListBuilder<Int, ColorEntity>(dataSourceFactory, config)
    }

    private fun invalidate() {
        dataSource.invalidate()
    }

    companion object {
        const val PAGE_SIZE = 8
    }
}

标签: androidpaginationandroid-pagingandroid-paging-library

解决方案


每次invalidate()调用时,整个列表将被视为无效并重新整体构建,创建一个新的DataSource实例。这实际上是预期的行为,但让我们逐步了解幕后发生的事情以了解问题:

  1. 创建了一个DataSource实例,并loadInitial调用了它的方法,其中包含零项(因为还没有存储数据)
  2. BoundaryCallback将被调用,因此将获取、存储第onZeroItemsLoaded一个数据,最后,它将使列表无效,因此将再次创建它。
  3. 将创建一个新的DataSourceloadInitial实例,再次调用它,但这一次,由于已经有一些数据,它将检索那些以前存储的项目。
  4. 用户将滚动到列表的底部,因此将尝试通过调用从DataSourceloadAfter加载一个新页面,这将检索 0 个项目,因为没有更多项目要加载。
  5. 所以BoundaryCallbackonItemAtEndLoaded将被调用,获取第二页,存储新项目,最后再次使整个列表无效。
  6. 同样,将创建一个新的DataSourceloadInitial ,再次调用它的,它只会检索第一个页面项目。
  7. 之后,一旦loadAfter再次调用 ,它现在将能够检索刚刚添加的新页面项目。
  8. 这将针对每一页进行。

此处的问题可以在步骤 6中确定。

问题是每次我们使DataSource无效时,它loadInitial只会检索第一个页面项目。loadAfter尽管已经存储了所有其他页面项,但新列表在调用它们的对应项之前不会知道它们的存在。因此,在获取一个新页面、存储它们的项目并使列表无效之后,会有一个时刻,新列表将仅由第一页项目组成(因为loadInitial只会检索那些)。这个新列表将提交给Adapter,因此,RecyclerView只会显示第一页项目,给人的印象是它又跳到了第一个项目。然而,现实情况是所有其他项目都已被删除,因为理论上它们不再在列表中。之后,一旦用户向下滚动,loadAfter就会调用相应的,并从存储的页面中再次检索页面项目,直到点击一个尚未存储项目的新页面,使其在存储后再次使整个列表失效新东西。

因此,为了避免这种情况,诀窍是loadInitial不仅要始终检索首页项目,还要检索所有已加载的项目。这样,一旦页面失效并调用了新的DataSource loadInitial新列表将不再仅由第一页项目组成,而是由所有已加载的项目组成,因此它们不会从RecyclerView中删除。

为此,我们可以跟踪已经加载了多少页面,以便我们可以告诉每个新的DataSource应该在loadInitial.


一个简单的解决方案是创建一个类来跟踪当前页面:

class PageTracker {
    var currentPage = 0
}

然后,修改自定义DataSource以接收此类的实例并更新它:

class ColorsDataSource(
    private val pageTracker: PageTracker
    private val colorsRepository: ColorsRepository
) : PageKeyedDataSource<Int, ColorEntity>() {

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, ColorEntity> 
    ) {
        //...
        val alreadyLoadedItems = (pageTracker.currentPage + 1) * params.requestedLoadSize
        val resultFromDB = colorsRepository.getColors(0, alreadyLoadedItems)
        callback.onResult(resultFromDB, null, pageTracker.currentPage + 1)
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ColorEntity>) {
        pageTracker.currentPage = params.key
        //...
    }

    //...
}

最后,创建一个实例PageTracker并将其传递给每个新的DataSource实例

dataSourceFactory = object : DataSource.Factory<Int, ColorEntity>() {

    val pageTracker = PageTracker()

    override fun create(): DataSource<Int, ColorEntity> {
        dataSource =  ColorsDataSource(pageTracker, repository)
        return dataSource
    }
}

注1

需要注意的是,如果需要再次刷新整个列表(由于拉动刷新操作或其他任何原因),则需要在使列表无效之前PageTracker将实例更新回。currentPage = 0


笔记2

同样重要的是要注意,使用Room时通常不需要这种方法,因为在这种情况下,我们可能不需要创建自定义DataSource,而是让Dao直接从查询中返回DataSource.Factory 。然后,当我们通过BoundaryCallback调用获取新数据并存储项目时, Room将自动使用所有项目更新我们的列表。


推荐阅读