如何在本地数据上正确使用自定义 PagingSource 和 PagingDataAdapter?
How can I correctly use custom PagingSource with PagingDataAdapter, on local data?
问题
我有本地生成的数据,需要在 RecyclerView
中显示。我尝试使用带有 PagingDataAdapter
的自定义 PagingSource
来减少内存中的数据量,但是当我使数据无效时,我会得到视觉效果,例如,如果我插入或删除一项:
- 当可见项属于 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 position。我试过 key = page number,每页包含 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
。
并在activity中收集并提交了数据页:
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 上的缓存
- 移除观察者并直接在insert/delete函数中使代码失效
- 而不是 key = position,使用 key = 页码 (0, 1, 2, ...) 每个页面包含 pageSize=30 项
此时,我不再确定 paging-3 是否用于自定义数据。我正在观察一个简单的 insert/delete 的许多操作,例如适配器中的 2000-4000 比较操作,重新加载 3 页数据,......直接在我的数据上使用 ListAdapter
并执行load/unload 手动似乎是更好的选择。
我终于找到了一个可能的解决方案,但我不确定如果更新 Paging-3 库是否可行。我还不知道上面解释的行为是否是由于 Paging-3 组件中的错误/限制造成的,或者这是否只是参考文档中的错误解释,或者我完全错过的东西。
我。 Work-around 故障问题。
在PagingConfig
中,我们必须有enablePlaceholders = true
,否则它根本无法正常工作。使用 false
我观察滚动条在滚动 up/down 时在每个 load
操作上跳跃,并且在末尾插入项目将使所有项目在显示上出现故障,然后列表将全部跳转通往顶峰的道路。
getRefreshKey
和 load
中的逻辑,如 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-generated PagingSource 的行为中推断出这一点,方法是在 DAO 代码和视图模型之间插入一个包装器以观察 params
和 LoadResult
值。
备注
LoadParam.Append
中的注释行模仿了 Room-generated 代码行为,在加载最后一页时该行为略有不正确。它使寻呼机在最后加载一个空页面,这不是一个严重的问题,但会触发整个链中不必要的操作。
- 我不确定
Refresh
案例,很难从 Room 的代码行为中推断出任何逻辑。在这里,我将 params.key
位置放在加载范围的中间(params.loadSize
项)。在最坏的情况下,它会在第二次加载操作中附加数据。
二.预测丢弃的数据
这应该可以从 params
赋予 load
功能。在简化的启发式中(范围必须 min/max 才能获得实际索引):
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
可以作为保留的范围,从那里很容易推导出什么被丢弃。
问题
我有本地生成的数据,需要在 RecyclerView
中显示。我尝试使用带有 PagingDataAdapter
的自定义 PagingSource
来减少内存中的数据量,但是当我使数据无效时,我会得到视觉效果,例如,如果我插入或删除一项:
- 当可见项属于 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 position。我试过 key = page number,每页包含 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
。
并在activity中收集并提交了数据页:
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 上的缓存
- 移除观察者并直接在insert/delete函数中使代码失效
- 而不是 key = position,使用 key = 页码 (0, 1, 2, ...) 每个页面包含 pageSize=30 项
此时,我不再确定 paging-3 是否用于自定义数据。我正在观察一个简单的 insert/delete 的许多操作,例如适配器中的 2000-4000 比较操作,重新加载 3 页数据,......直接在我的数据上使用 ListAdapter
并执行load/unload 手动似乎是更好的选择。
我终于找到了一个可能的解决方案,但我不确定如果更新 Paging-3 库是否可行。我还不知道上面解释的行为是否是由于 Paging-3 组件中的错误/限制造成的,或者这是否只是参考文档中的错误解释,或者我完全错过的东西。
我。 Work-around 故障问题。
在
PagingConfig
中,我们必须有enablePlaceholders = true
,否则它根本无法正常工作。使用false
我观察滚动条在滚动 up/down 时在每个load
操作上跳跃,并且在末尾插入项目将使所有项目在显示上出现故障,然后列表将全部跳转通往顶峰的道路。getRefreshKey
和load
中的逻辑,如 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-generated PagingSource 的行为中推断出这一点,方法是在 DAO 代码和视图模型之间插入一个包装器以观察 params
和 LoadResult
值。
备注
LoadParam.Append
中的注释行模仿了 Room-generated 代码行为,在加载最后一页时该行为略有不正确。它使寻呼机在最后加载一个空页面,这不是一个严重的问题,但会触发整个链中不必要的操作。- 我不确定
Refresh
案例,很难从 Room 的代码行为中推断出任何逻辑。在这里,我将params.key
位置放在加载范围的中间(params.loadSize
项)。在最坏的情况下,它会在第二次加载操作中附加数据。
二.预测丢弃的数据
这应该可以从 params
赋予 load
功能。在简化的启发式中(范围必须 min/max 才能获得实际索引):
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
可以作为保留的范围,从那里很容易推导出什么被丢弃。