后记
法一
突然想到可以在LaunchedEffect里使用refresh方法,在ViewModel里用一个State来在非Compose层控制刷新,根本不需要这么麻烦,真是笨蛋QAQ
法二
var pagingSource: WpCommentPagingSource? = null
val pager = Pager(config = PagingConfig(20), initialKey = 1) {
WpCommentPagingSource().also {
pagingSource = it
}
}
然后调用pagingSource?.invalidate()即可刷新
法三
前言
一般情况下,我们无法在非compose环境下刷新Paging。在compose中,我们可以在collect已缓存的只读流以后,调用refresh方法刷新。 这会调用当前的 PagingSource.invalidate(),Paging框架会自动重新加载第一页。
需求
有时候我们希望在ViewModel里封装刷新方法,在UI里调用自己封装的刷新函数,使代码更简洁。
方法
想刷新分页数据(即重新加载第一页),必须重新触发一个新的 Pager 实例或重新收集 Flow
// 刷新触发器,带防抖
private val refreshTrigger = MutableStateFlow(0L)
// 用于标识每次刷新的唯一ID
private val refreshId = AtomicInteger(0)
private var currentRefreshId = 0
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
val resListPagingFlow = refreshTrigger
.debounce(100) // 100ms 防抖,避免过快点击
.flatMapLatest {
val thisRefreshId = currentRefreshId
Log.d(TAG, "flatMapLatest: creating new Pager for refreshId=$thisRefreshId")
Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 20
),
initialKey = 1,
pagingSourceFactory = {
Log.d(TAG, "pagingSourceFactory: creating PagingSource for refreshId=$thisRefreshId")
MainResPostPagingSource(
resListUrl = resListUrl,
repository = repository,
refreshId = thisRefreshId,
onFinished = { completedRefreshId, isFirstPage ->
// 只有当前刷新ID匹配且是第一页加载完成时,才关闭刷新状态
if (completedRefreshId == currentRefreshId && isFirstPage) {
isRefreshing.value = false
Log.d(TAG, "onFinished: refreshId=$completedRefreshId matches, closing refresh indicator")
} else {
Log.d(TAG, "onFinished: refreshId=$completedRefreshId, current=$currentRefreshId, isFirstPage=$isFirstPage, ignoring (outdated request)")
}
}
)
}
).flow
}.cachedIn(viewModelScope)
先定义一个触发器refreshTrigger, 每次改变它,flatMapLatest会取消旧 flow 并开始新 flow。我们使用refreshTrigger.value += 1 就可以刷新Pager
说明:flatMapLatest的用途是把一个 Flow 转换成另一个 Flow,并在上游更新时取消旧的下游 Flow(不要和列表的flatMap混淆,这个跟flatMap没有关系)
这时候其实就已经完成compose外刷新的功能了。但是在我的使用场景,点击TabRow的item会在刷新的时候传入不同的url,请求不同的数据源列表。
后续问题
异步请求完成后会修改 isRefreshing ,如果此时Tab切换太快,导致可能有多个请求同时进行;使用 refreshTrigger.value += 1 触发新的 Pager Flow,但之前的请求可能还在进行,导致数据错乱;旧的请求完成后会把 isRefreshing 设为 false,导致刷新图标提前收回
解决方案
首先想到了使用Job,在刷新请求执行请求之前,先取消上一个请求。但在这里我们刷新只是触发了触发器,无法将实际异步执行放入Job里
实际上,flatMapLatest会在上游状态更新的时候取消当前flow,这时候只需要在PagingSource的load方法里,异步请求后使用coroutineContext.ensureActive()来确认协程是否取消,就可以避免数据错乱问题
接着是下拉刷新指示器提前消失的问题,想到使用最简单的id来判断是否为最后一次刷新。每次请求生成一个独立id存储到当前id并传入PagingSource,当请求结束以后判断id是否和当前id匹配,若不匹配说明后续还进行了其它请求,故不取消刷新状态。
其实还有一些细节问题,如上划加载更多尚未测试,不过主要问题已经解决
补充
.cachedIn(viewModelScope)要放在外层嵌套,而不是紧跟.flow,避免切换到别的页面,再回来时触发重新collect导致列表刷新