android - 无效的自定义 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
}
}
解决方案
每次invalidate()
调用时,整个列表将被视为无效并重新整体构建,创建一个新的DataSource实例。这实际上是预期的行为,但让我们逐步了解幕后发生的事情以了解问题:
- 创建了一个DataSource实例,并
loadInitial
调用了它的方法,其中包含零项(因为还没有存储数据) - BoundaryCallback将被调用,因此将获取、存储第
onZeroItemsLoaded
一个数据,最后,它将使列表无效,因此将再次创建它。 - 将创建一个新的DataSource
loadInitial
实例,再次调用它,但这一次,由于已经有一些数据,它将检索那些以前存储的项目。 - 用户将滚动到列表的底部,因此将尝试通过调用从DataSource
loadAfter
加载一个新页面,这将检索 0 个项目,因为没有更多项目要加载。 - 所以BoundaryCallback
onItemAtEndLoaded
将被调用,获取第二页,存储新项目,最后再次使整个列表无效。 - 同样,将创建一个新的DataSource
loadInitial
,再次调用它的,它只会检索第一个页面项目。 - 之后,一旦
loadAfter
再次调用 ,它现在将能够检索刚刚添加的新页面项目。 - 这将针对每一页进行。
此处的问题可以在步骤 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将自动使用所有项目更新我们的列表。
推荐阅读
- python-3.x - 如何正确使用嵌套循环
- kotlin - 如何修复未解决的参考:setupWithViewPager
- android - firebasetool的recyclerview聊天UI
- swift - 类型“DBTweet”不符合协议“可解码”
- php - 有没有办法在表单本身上显示错误但表单操作重定向到另一个 php 文件?
- android - 在改造获取请求 A 中将数组作为参数发送
- java - 与 Google Guava 的范围联合
- c# - C# - 将 Json 反序列化为字典
- python - 如何正确检查碰撞?
- postgresql - 从标准 App Engine 环境迁移到 flex App Engine 环境,现在 Cloud SQL 将无法连接