使自定义 PageKeyedDataSource 无效会使回收器视图跳转
Invalidating custom PageKeyedDataSource makes recycler view jump
我正在尝试使用自定义 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
}
}
每次调用invalidate()
时,整个列表将被视为无效并重新构建,创建一个新的DataSource实例。这实际上是预期的行为,但让我们逐步了解引擎盖下发生的事情以了解问题:
- 创建了一个 DataSource 实例,并调用了它的
loadInitial
方法,其中的项目为零(因为还没有存储数据)
- BoundaryCallback 的
onZeroItemsLoaded
将被调用,因此将首先获取、存储数据,最后,它会使列表无效,因此将再次创建它.
- 一个新的 DataSource 实例将被创建,再次调用它的
loadInitial
,但是这次,因为已经有一些数据,它将检索那些以前存储的项目.
- 用户将滚动到列表底部,因此将尝试通过调用
loadAfter
从 DataSource 加载新页面,这将检索 0 个项目没有更多的项目要加载。
- 因此 BoundaryCallback 中的
onItemAtEndLoaded
将被调用,获取第二页,存储新项目并最终再次使整个列表无效。
- 同样,将创建一个新的 DataSource,再次调用它的
loadInitial
,这将只检索第一页的项目。
- 之后,再次调用
loadAfter
后,它现在将能够检索刚刚添加的新页面项目。
- 每一页都会如此。
这里的问题可以在步骤 6 中找到。
问题是每次我们使 DataSource 无效时,它的 loadInitial
只会检索第一页的项目。尽管已经存储了所有其他页面项目,但新列表在调用相应的 loadAfter
之前不会知道它们的存在。
因此,在获取新页面、存储它们的项目并使列表无效后,会有一段时间新列表将仅由第一页项目组成(因为 loadInitial
只会检索这些项目)。这个新列表将提交给 Adapter,因此 RecyclerView 只会显示第一页项目,给人的印象是它跳到了又是第一项。然而,事实是所有其他项目都已被删除,因为理论上它们已不在列表中。之后,一旦用户向下滚动,就会调用相应的loadAfter
,并从存储的页面中重新获取页面项,直到找到一个没有存储项的新页面,使整个页面失效存储新项目后再次列出。
因此,为了避免这种情况,诀窍是让 loadInitial
不仅总是检索第一页项目,而且还检索所有 已加载的项目 。这样,一旦页面无效并调用新的 DataSource 的 loadInitial
,新列表将不再仅由第一页项目组成,而是由所有已经加载的,这样它们就不会从 RecyclerView.
中删除
为此,我们可以跟踪已经加载了多少页面,这样我们就可以告诉每个新的 DataSources 应该在 loadInitial
.
一个简单的解决方案是创建一个 class 来跟踪当前页面:
class PageTracker {
var currentPage = 0
}
然后,修改自定义 DataSource 以接收此 class 的实例并更新它:
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 return DataSource.Factory。然后,当我们由于 BoundaryCallback 调用获取新数据并存储项目时,Room 将自动更新我们的列表 all 项。
在 DiffUtilCallback
中 areItemsTheSame
比较 ID 而不是引用:
override fun areItemsTheSame(oldItem: ColorEntity, newItem: ColorEntity): Boolean
= oldItem.db_id == newItem.db_id
这样 recyclerView
将从 id 而不是引用中找到以前的位置。
我正在尝试使用自定义 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
}
}
每次调用invalidate()
时,整个列表将被视为无效并重新构建,创建一个新的DataSource实例。这实际上是预期的行为,但让我们逐步了解引擎盖下发生的事情以了解问题:
- 创建了一个 DataSource 实例,并调用了它的
loadInitial
方法,其中的项目为零(因为还没有存储数据) - BoundaryCallback 的
onZeroItemsLoaded
将被调用,因此将首先获取、存储数据,最后,它会使列表无效,因此将再次创建它. - 一个新的 DataSource 实例将被创建,再次调用它的
loadInitial
,但是这次,因为已经有一些数据,它将检索那些以前存储的项目. - 用户将滚动到列表底部,因此将尝试通过调用
loadAfter
从 DataSource 加载新页面,这将检索 0 个项目没有更多的项目要加载。 - 因此 BoundaryCallback 中的
onItemAtEndLoaded
将被调用,获取第二页,存储新项目并最终再次使整个列表无效。 - 同样,将创建一个新的 DataSource,再次调用它的
loadInitial
,这将只检索第一页的项目。 - 之后,再次调用
loadAfter
后,它现在将能够检索刚刚添加的新页面项目。 - 每一页都会如此。
这里的问题可以在步骤 6 中找到。
问题是每次我们使 DataSource 无效时,它的 loadInitial
只会检索第一页的项目。尽管已经存储了所有其他页面项目,但新列表在调用相应的 loadAfter
之前不会知道它们的存在。
因此,在获取新页面、存储它们的项目并使列表无效后,会有一段时间新列表将仅由第一页项目组成(因为 loadInitial
只会检索这些项目)。这个新列表将提交给 Adapter,因此 RecyclerView 只会显示第一页项目,给人的印象是它跳到了又是第一项。然而,事实是所有其他项目都已被删除,因为理论上它们已不在列表中。之后,一旦用户向下滚动,就会调用相应的loadAfter
,并从存储的页面中重新获取页面项,直到找到一个没有存储项的新页面,使整个页面失效存储新项目后再次列出。
因此,为了避免这种情况,诀窍是让 loadInitial
不仅总是检索第一页项目,而且还检索所有 已加载的项目 。这样,一旦页面无效并调用新的 DataSource 的 loadInitial
,新列表将不再仅由第一页项目组成,而是由所有已经加载的,这样它们就不会从 RecyclerView.
为此,我们可以跟踪已经加载了多少页面,这样我们就可以告诉每个新的 DataSources 应该在 loadInitial
.
一个简单的解决方案是创建一个 class 来跟踪当前页面:
class PageTracker {
var currentPage = 0
}
然后,修改自定义 DataSource 以接收此 class 的实例并更新它:
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 return DataSource.Factory。然后,当我们由于 BoundaryCallback 调用获取新数据并存储项目时,Room 将自动更新我们的列表 all 项。
在 DiffUtilCallback
中 areItemsTheSame
比较 ID 而不是引用:
override fun areItemsTheSame(oldItem: ColorEntity, newItem: ColorEntity): Boolean
= oldItem.db_id == newItem.db_id
这样 recyclerView
将从 id 而不是引用中找到以前的位置。