androidx.paging 3.3.6 遇到的一些坑

参考示例: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.手动计算偏移量(过时方案)

  1. PagingSource 返回的Bean包装一下,携带这个数据的真实offset
  2. 计算count时加上data中第一个Bean的offset
  3. 从data中取Bean时减掉第一个Bean的offset
  4. 处理边界值问题:防止越界、数量错误
// ===== 数据加载 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")
    }
}