Android Paging 3 库 PagingSource 失效,导致列表因刷新键错误而跳转(未使用房间)
Android Paging 3 library PagingSource invalidation, causes the list to jump due to wrong refresh key (not using room)
由于我目前正在处理一个带有自定义数据库(不是 Room)的项目,我正在测试我们是否可以在该项目中使用 Paging 3 库。
但是,我 运行 关注这个问题,如果您更改数据并因此使分页源无效,则重新创建列表会出现错误并跳转到其他位置。发生这种情况是因为 Refresh Key 计算似乎有误,这很可能是由于初始加载加载了三页数据,但将其放入了一页。
默认的分页源如下所示:
override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? {
// Try to find the page key of the closest page to anchorPosition, from
// either the prevKey or the nextKey, but you need to handle nullability
// here:
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey null -> anchorPage is the initial page, so
// just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomData> {
var pagePosition = params.key ?: STARTING_PAGE_INDEX
var loadSize = params.loadSize
return try {
val dataResult = dataRepository.getPagedData(
pagePosition = pagePosition,
loadSize = loadSize,
pageSize = pageSize
)
val nextKey = if (dataResult.isEmpty() || dataResult.size < pageSize) {
null
} else {
pagePosition + (loadSize / pageSize)
}
Log.i(
"RoomFreePagingSource",
"page $pagePosition with size $loadSize publish ${dataResult.size} routes"
)
return LoadResult.Page(
data = dataResult,
prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
dataRepository.getPagedData()
函数只是访问内存列表和 return 列表的一个子部分,模拟分页数据。
为了完整起见,这里是这个函数的实现:
fun getPagedData(pageSize:Int, loadSize:Int, pagePosition: Int): List<CustomData>{
val startIndex = pagePosition * pageSize
val endIndexExl =startIndex + loadSize
return data.safeSubList(startIndex,endIndexExl).map { it.copy() }
}
private fun <T> List<T>.safeSubList(fromIndex: Int, toIndex: Int) : List<T>{
// only returns list with valid range, to not throw exception
if(fromIndex>= this.size)
return emptyList()
val endIndex = if(toIndex> this.size) this.size else toIndex
return subList(fromIndex,endIndex)
}
我目前面临的主要问题是,getRefreshKey
函数没有 return 正确的刷新页面键,这导致错误的页面被刷新并且列表跳转到加载页。
- 例如,如果您每页有 10 个项目。
- 第一页 0 包含 30 项
- 下一页为 3,包含 10 个项目。
- 如果您不滚动(只看到前 7 个项目)并且无效,则
anchorPosition
将为 7,刷新键将为 2。
(anchorPage.prevKey = null => anchorPage.nextKey = 3 => 3-1 是 2)
但是,此时我们想加载第 0 页而不是第 2 页。
知道问题的原因后,我尝试调整默认实现来解决它,并想出了很多不同的版本。下面的那个目前效果最好,但仍然会不时导致跳跃。并且有时列表的一部分会闪烁,这可能是因为没有足够的项目被提取来填充视口。
override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val closestPage = state.closestPageToPosition(anchorPosition)?.prevKey
?: STARTING_PAGE_INDEX
val refKey = if(anchorPosition>(closestPage)*pageSize + pageSize)
closestPage+1
else
closestPage
Log.i(
"RoomFreePagingSource",
"getRefreshKey $refKey from anchorPosition $anchorPosition closestPage $closestPage"
)
refKey
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomData> {
var pagePosition = params.key ?: STARTING_PAGE_INDEX
var loadSize = pageSize
return try {
when (params) {
is LoadParams.Refresh -> {
if (pagePosition > STARTING_PAGE_INDEX) {
loadSize *= 3
} else if (pagePosition == STARTING_PAGE_INDEX) {
loadSize *= 2
}
}
else -> {}
}
val dataResult = dataRepository.getPagedData(
pagePosition = pagePosition,
loadSize = loadSize,
pageSize = pageSize
)
val nextKey = if (dataResult.isEmpty() || dataResult.size < pageSize) {
null
} else {
pagePosition + (loadSize / pageSize)
}
Log.i(
"RouteRepository",
"page $pagePosition with size $loadSize publish ${dataResult.size} routes"
)
return LoadResult.Page(
data = dataResult,
prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
理论上我发现最好的解决方案是像这样计算刷新 PageKey anchorPosition/pageSize
然后加载此页面,即它之前和之后的页面。但是,像这样计算刷新键不起作用,因为 anchorPosition 不是列表中项目的实际位置。在一些失效之后,锚点可能是 5,即使您当前正在查看列表中的第 140 项。
总结一下:
当我不使用 Room 而是使用另一个数据源时,如何在失效后计算正确的刷新页面,例如在这个示例案例中我通过 getPagedData
访问的内存列表?
您可以在创建Pager 时在PagingConfig 对象中定义初始加载大小。喜欢
Pager(
config = PagingConfig(pageSize = PAGE_SIZE, initialLoadSize = PAGE_SIZE),
initialKey = INITIAL_PAGE,
remoteMediator = mediator,
pagingSourceFactory = {
...
})
同时找到了自己的解决方案,但忘记在此处添加答案。
getRefreshKey()
中的anchorPosition
是错误的,因为没有为分页源返回的每个Page设置itemsBefore
参数。这种行为似乎不是 documented.
LoadResult.Page(
itemsBefore = pagePosition * pageSize,
data = dataResult,
prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1,
nextKey = nextKey
)
现在实际上可以根据 anchorPosition
和 pageSize
(在本例中定义为 PagingSource
构造函数参数来确定刷新键,但也可以是一个常数等)
override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? {
return state.anchorPosition?.let { anchorPosition ->
anchorPosition / pageSize
}
}
否列表在刷新/失效时不再跳转。然而,有时列表可能会闪烁,或者列表的整个部分在失效时不可见。这是因为刷新页面太小,没有覆盖整个视口。因此,可以看到刷新页面之外的项目正在执行分页。可以通过将刷新页面 (loadSize
) 扩大 3 倍并将 pagePosition
减一来修复此行为。 3次是必要的,因为实际刷新页面之前和之后的页面可以在视口中看到。
if (params is LoadParams.Refresh) {
loadSize *= 3
pagePosition = max(0, pagePosition - 1)
}
这个解决方案现在工作得很好,但是,感觉这不能/不应该成为这个问题的官方解决方案。因为在使用寻呼和房间时不需要所有这些调整。所以,我愿意接受其他/更好的解决方案。
这里是工作 PagingSource 的完整代码,包含所有提到的更改:
override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? {
return state.anchorPosition?.let { anchorPosition ->
anchorPosition / pageSize
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomData> {
var pagePosition = params.key ?: STARTING_PAGE_INDEX
var loadSize = pageSize
return try {
if (params is LoadParams.Refresh) {
// make sure everything visible in the view port is updated / loaded
loadSize *= 3
pagePosition = max(0, pagePosition - 1)
}
val dataResult = dataRepository.getPagedData(
pagePosition = pagePosition,
loadSize = loadSize,
pageSize = pageSize
)
val nextKey = if (dataResult.isEmpty() || dataResult.size < pageSize) {
null
} else {
pagePosition + (loadSize / pageSize)
}
LoadResult.Page(
itemsBefore = pagePosition * pageSize,
data = dataResult,
prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
由于我目前正在处理一个带有自定义数据库(不是 Room)的项目,我正在测试我们是否可以在该项目中使用 Paging 3 库。 但是,我 运行 关注这个问题,如果您更改数据并因此使分页源无效,则重新创建列表会出现错误并跳转到其他位置。发生这种情况是因为 Refresh Key 计算似乎有误,这很可能是由于初始加载加载了三页数据,但将其放入了一页。
默认的分页源如下所示:
override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? {
// Try to find the page key of the closest page to anchorPosition, from
// either the prevKey or the nextKey, but you need to handle nullability
// here:
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey null -> anchorPage is the initial page, so
// just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomData> {
var pagePosition = params.key ?: STARTING_PAGE_INDEX
var loadSize = params.loadSize
return try {
val dataResult = dataRepository.getPagedData(
pagePosition = pagePosition,
loadSize = loadSize,
pageSize = pageSize
)
val nextKey = if (dataResult.isEmpty() || dataResult.size < pageSize) {
null
} else {
pagePosition + (loadSize / pageSize)
}
Log.i(
"RoomFreePagingSource",
"page $pagePosition with size $loadSize publish ${dataResult.size} routes"
)
return LoadResult.Page(
data = dataResult,
prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
dataRepository.getPagedData()
函数只是访问内存列表和 return 列表的一个子部分,模拟分页数据。
为了完整起见,这里是这个函数的实现:
fun getPagedData(pageSize:Int, loadSize:Int, pagePosition: Int): List<CustomData>{
val startIndex = pagePosition * pageSize
val endIndexExl =startIndex + loadSize
return data.safeSubList(startIndex,endIndexExl).map { it.copy() }
}
private fun <T> List<T>.safeSubList(fromIndex: Int, toIndex: Int) : List<T>{
// only returns list with valid range, to not throw exception
if(fromIndex>= this.size)
return emptyList()
val endIndex = if(toIndex> this.size) this.size else toIndex
return subList(fromIndex,endIndex)
}
我目前面临的主要问题是,getRefreshKey
函数没有 return 正确的刷新页面键,这导致错误的页面被刷新并且列表跳转到加载页。
- 例如,如果您每页有 10 个项目。
- 第一页 0 包含 30 项
- 下一页为 3,包含 10 个项目。
- 如果您不滚动(只看到前 7 个项目)并且无效,则
anchorPosition
将为 7,刷新键将为 2。 (anchorPage.prevKey = null => anchorPage.nextKey = 3 => 3-1 是 2) 但是,此时我们想加载第 0 页而不是第 2 页。
知道问题的原因后,我尝试调整默认实现来解决它,并想出了很多不同的版本。下面的那个目前效果最好,但仍然会不时导致跳跃。并且有时列表的一部分会闪烁,这可能是因为没有足够的项目被提取来填充视口。
override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val closestPage = state.closestPageToPosition(anchorPosition)?.prevKey
?: STARTING_PAGE_INDEX
val refKey = if(anchorPosition>(closestPage)*pageSize + pageSize)
closestPage+1
else
closestPage
Log.i(
"RoomFreePagingSource",
"getRefreshKey $refKey from anchorPosition $anchorPosition closestPage $closestPage"
)
refKey
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomData> {
var pagePosition = params.key ?: STARTING_PAGE_INDEX
var loadSize = pageSize
return try {
when (params) {
is LoadParams.Refresh -> {
if (pagePosition > STARTING_PAGE_INDEX) {
loadSize *= 3
} else if (pagePosition == STARTING_PAGE_INDEX) {
loadSize *= 2
}
}
else -> {}
}
val dataResult = dataRepository.getPagedData(
pagePosition = pagePosition,
loadSize = loadSize,
pageSize = pageSize
)
val nextKey = if (dataResult.isEmpty() || dataResult.size < pageSize) {
null
} else {
pagePosition + (loadSize / pageSize)
}
Log.i(
"RouteRepository",
"page $pagePosition with size $loadSize publish ${dataResult.size} routes"
)
return LoadResult.Page(
data = dataResult,
prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
理论上我发现最好的解决方案是像这样计算刷新 PageKey anchorPosition/pageSize
然后加载此页面,即它之前和之后的页面。但是,像这样计算刷新键不起作用,因为 anchorPosition 不是列表中项目的实际位置。在一些失效之后,锚点可能是 5,即使您当前正在查看列表中的第 140 项。
总结一下:
当我不使用 Room 而是使用另一个数据源时,如何在失效后计算正确的刷新页面,例如在这个示例案例中我通过 getPagedData
访问的内存列表?
您可以在创建Pager 时在PagingConfig 对象中定义初始加载大小。喜欢
Pager(
config = PagingConfig(pageSize = PAGE_SIZE, initialLoadSize = PAGE_SIZE),
initialKey = INITIAL_PAGE,
remoteMediator = mediator,
pagingSourceFactory = {
...
})
同时找到了自己的解决方案,但忘记在此处添加答案。
getRefreshKey()
中的anchorPosition
是错误的,因为没有为分页源返回的每个Page设置itemsBefore
参数。这种行为似乎不是 documented.LoadResult.Page( itemsBefore = pagePosition * pageSize, data = dataResult, prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1, nextKey = nextKey )
现在实际上可以根据
anchorPosition
和pageSize
(在本例中定义为PagingSource
构造函数参数来确定刷新键,但也可以是一个常数等)override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? { return state.anchorPosition?.let { anchorPosition -> anchorPosition / pageSize } }
否列表在刷新/失效时不再跳转。然而,有时列表可能会闪烁,或者列表的整个部分在失效时不可见。这是因为刷新页面太小,没有覆盖整个视口。因此,可以看到刷新页面之外的项目正在执行分页。可以通过将刷新页面 (
loadSize
) 扩大 3 倍并将pagePosition
减一来修复此行为。 3次是必要的,因为实际刷新页面之前和之后的页面可以在视口中看到。if (params is LoadParams.Refresh) { loadSize *= 3 pagePosition = max(0, pagePosition - 1) }
这个解决方案现在工作得很好,但是,感觉这不能/不应该成为这个问题的官方解决方案。因为在使用寻呼和房间时不需要所有这些调整。所以,我愿意接受其他/更好的解决方案。
这里是工作 PagingSource 的完整代码,包含所有提到的更改:
override fun getRefreshKey(state: PagingState<Int, CustomData>): Int? {
return state.anchorPosition?.let { anchorPosition ->
anchorPosition / pageSize
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomData> {
var pagePosition = params.key ?: STARTING_PAGE_INDEX
var loadSize = pageSize
return try {
if (params is LoadParams.Refresh) {
// make sure everything visible in the view port is updated / loaded
loadSize *= 3
pagePosition = max(0, pagePosition - 1)
}
val dataResult = dataRepository.getPagedData(
pagePosition = pagePosition,
loadSize = loadSize,
pageSize = pageSize
)
val nextKey = if (dataResult.isEmpty() || dataResult.size < pageSize) {
null
} else {
pagePosition + (loadSize / pageSize)
}
LoadResult.Page(
itemsBefore = pagePosition * pageSize,
data = dataResult,
prevKey = if (pagePosition == STARTING_PAGE_INDEX) null else pagePosition - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}