参考示例:Kemono - PostDetailScreen.kt PagingSource.kt
遇到的问题
data.itemCount
代表的是已缓存的数据大小,并不是真实偏移量。
如果Pager.initialKey
不在第一页的范围内,UI将无法跳转到第二页及以后的位置
不需要额外处理的特殊场景
- 数据库:Room数据库返回的 PagingSource
- 通过List,Array等本地数据包装的PagingData
这些都是全量数据,itemCount永远是和offset一致的,所以只需传入initialKey就可以了
目前可行的解决方案
1. 在 PagingSource.load
返回数据时返回 itemsBefore
LoadResult.Page(result, prev, next, itemsBefore = offset)
这样Paging就可以知道要初始化多大的空列表了,此时 data.itemCount
等于 offset
2.手动计算偏移量(过时方案)
- PagingSource 返回的Bean包装一下,携带这个数据的真实offset
- 计算count时加上data中第一个Bean的offset
- 从data中取Bean时减掉第一个Bean的offset
- 处理边界值问题:防止越界、数量错误
// ===== 数据加载 PostDetailScreenViewModel =====
var data: Flow<PagingData<KemonoPostWrapper>> by mutableStateOf(emptyFlow())
// 网络请求加载数据
data = Pager(
config = PagingConfig(KEMONO_PAGE_SIZE),
initialKey = offset.floorMultiple(KEMONO_PAGE_SIZE)
) {
ArtistPostPagingSource(service, artistId, noCache)
}.flow.cachedIn(viewModelScope)
// ===== 数据源 ArtistPostPagingSource =====
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, KemonoPostWrapper> {
val offset = params.key
if (offset == null) {
"params.key == null".loge()
return LoadResult.Error(Exception("Key is null"))
}
withContext(Dispatchers.IO) {
"load start $service/$artistId offset: $offset".logi(TAG)
KemonoApi.artistDetail(service, artistId, offset)
}.onSuccess {
val prev = if (offset == 0) null else (offset - it.size).coerceAtLeast(0)
val next = if (it.isEmpty() || it.size < KEMONO_PAGE_SIZE) null else offset + it.size
preloadImage(it.mapNotNull { it.file?.thumbnail }.filter { it.isImage() })
// data.wrap(offset + idx) 包装了真实偏移量
val result = it.mapIndexed { idx, data -> data.wrap(offset + idx) }
"load success $service/$artistId offset: $offset size: ${it.size} $prev/$next".logi(TAG)
return LoadResult.Page(result, prev, next)
}.onFailure {
"load failed $service/$artistId offset: $offset".logi(TAG)
it.printStackTrace()
return LoadResult.Error(it)
}
return LoadResult.Error(Exception("never happen"))
}
// 由于无法在外部得到真实offset,所以需要创建一个包装数据类,将偏移量和源数据一起打包
@Serializable
data class KemonoPostWrapper(
val offset: Int,
val data: KemonoPost
)
fun KemonoPost.wrap(offset: Int = 0) = KemonoPostWrapper(offset, this)
// ===== UI界面展示 PostDetailScreen =====
// 真实pageCount 获取,需要加上data中第一个偏移量
val data = vm.data.collectAsLazyPagingItems()
var lastPageCount by remember { mutableStateOf(-1) }
val pagerState = rememberPagerState(
initialPage = offset,
) {
val pagerOffset = data.itemSnapshotList.firstOrNull()?.offset ?: 0
// 这里一定要 coerceAtLeast(offset + 1) 否则会导致因初始化数量不足列表自动滚到最后一项
(data.itemCount + pagerOffset).coerceAtLeast(offset + 1).also {
if (it != lastPageCount) {
lastPageCount = it
"pageCount = $it".logi("PostDetailScreen")
}
}
}
// 数据展示 result 就是我们请求结果的bean
val result = synchronized(data.itemSnapshotList) {
val pagerOffset = data.itemSnapshotList.firstOrNull()?.offset ?: 0
val realIndex = index - pagerOffset
if (realIndex !in data.itemSnapshotList.indices) null else data[realIndex].also {
"$index/$realIndex/${data.itemCount} $offset ${it == null}".logi("PostDetailScreen")
}
}