无效的自定义PageKeyedDataSource使回收者视图跳转 [英] Invalidating custom PageKeyedDataSource makes recycler view jump

查看:121
本文介绍了无效的自定义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
    }
}

BoundaryCallback

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
        }
    }

ViewModel

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
    }
}

解决方案

每次调用invalidate()时,整个列表都将被视为无效并重新进行整体构建,从而创建一个新的 DataSource 实例.实际上,这是预期的行为,但让我们一步一步地了解幕后情况,以了解问题所在:

  1. 创建一个 DataSource 实例,并调用其loadInitial方法,该方法的项为零(因为尚未存储任何数据)
  2. BoundaryCallback onZeroItemsLoaded将被调用,因此将首先获取,存储并最终使列表无效,因此将再次创建该数据.
  3. 将创建一个新的 DataSource 实例,再次调用其loadInitial,但是这次,由于已经有一些数据,它将检索那些以前存储的项目.
  4. 用户将滚动到列表的底部,因此将尝试通过调用loadAfter DataSource 加载新页面,该页面将检索0个项目,因为没有其他项目可已加载.
  5. 因此,将调用 BoundaryCallback 中的onItemAtEndLoaded,获取第二页,存储新项目,最后再次使整个列表无效.
  6. 同样,将创建一个新的 DataSource ,再次调用其loadInitial,它将仅检索第一页项目.
  7. 之后,一旦再次调用loadAfter,它现在将能够检索刚刚添加的新页面项目.
  8. 这将在每个页面上继续进行.

此处的问题可以在第6步中找到.

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

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

为此,我们可以跟踪已经加载了多少页,以便可以告诉每个新的 DataSources 应该在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

还需要注意的是,使用

BoundaryCallback

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)
    }
}

Adapter's 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
        }
    }

ViewModel

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
    }
}

Every time invalidate() is called, the whole list will be considered invalid and built again in its whole, creating a new DataSource instance. It is actually the expected behaviour, but lets see a step by step sequence of what is happening under the hood to understand the problem:

  1. A DataSource instance is created, and its loadInitial method is called, with zero items (As there is no data stored yet)
  2. BoundaryCallback's onZeroItemsLoaded will be called, so first data will be fetched, stored and finally, it will invalidate the list, so it will be created again.
  3. A new DataSource instance will be created, calling its loadInitial again, but this time, as there is already some data, it will retrieve those previously stored items.
  4. User will scroll to the list's bottom, so a new page will be tried to be loaded from the DataSource by calling loadAfter, which will retrieve 0 items as there are no more items to be loaded.
  5. So onItemAtEndLoaded in BoundaryCallback will be called, fetching the second page, storing the new items and finally invalidating the whole list again.
  6. Again, a new DataSource will be created, calling once more its loadInitial, which will only retrieve the first page items.
  7. After, once the loadAfter is called again, it will now be able to retrieve the new page items as they have just been added.
  8. This will go on for each page.

The problem here can be identified at Step 6.

The thing is that every time we invalidate the DataSource, its loadInitial will only retrieve the first page items. Although having all the other pages items already stored, the new list will not know about their existence until their corresponding loadAfter is called. So after fetching a new page, storing their items and invalidating the list, there will be a moment in which the new list will only be composed by the first page items (as loadInitial will only retrieve those). This new list will be submitted to the Adapter, and so, the RecyclerView will only show the first page items, giving the impression it jumped up to the first item again. However, the reality is that all the other items have been removed as, in theory, they are no longer in the list. After that, once the user scrolls down, the corresponding loadAfter will be called, and the page items will be retrieved again from the stored ones, until a new page with no stored items yet is hit, making it invalidate the whole list again after storing the new items.

So, in order to avoid this, the trick is to make loadInitial not just always retrieve the first page items, but all the already loaded items. This way, once the page is invalidated and the new DataSource's loadInitial is called, the new list will no longer be composed by just the first page items, but by all the already loaded ones, so that they are not removed from the RecyclerView.

To do so, we could keep track of how many pages have already been loaded, so that we can tell to each new DataSources how many of them should be retrieved at loadInitial.


A simple solution would consist on creating a class to keep track of the current page:

class PageTracker {
    var currentPage = 0
}

Then, modify the custom DataSource to receive an instance of this class and update it:

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
        //...
    }

    //...
}

Finally, create an instance of PageTrackerand pass it to each new DataSource instance

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

    val pageTracker = PageTracker()

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


NOTE 1

It is important to note that if it is needed to refresh the whole list again (due to a pull-to-refresh action or anything else), PageTracker instance will be required to be updated back to currentPage = 0 before invalidating the list.


NOTE 2

It is also important to note that this approach is usually not required when using Room, as in this case we probably do not need to create our custom DataSource, but instead make the Dao directly return the DataSource.Factory directly from the query. Then, when we fetch new data due to BoundaryCallback calls and store the items, Room will automatically update our list with all the items.

这篇关于无效的自定义PageKeyedDataSource使回收者视图跳转的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆