为 UI 准备 PagingData
data class UiState(
val query: String,
val searchResult: RepoSearchResult
)
sealed class RepoSearchResult {
data class Success(val data: List<Repo>) : RepoSearchResult()
data class Error(val error: Exception) : RepoSearchResult()
}
class SearchRepositoriesViewModel(
private val repository: GithubRepository,
…
) : ViewModel() {
…
private fun searchRepo(queryString: String): Flow<PagingData<Repo>> =
repository.getSearchResultStream(queryString)
}
class SearchRepositoriesViewModel(
…
ViewModel() { :
val state: StateFlow<UiState>
val pagingDataFlow: Flow<PagingData<Repo>>
init {
…
pagingDataFlow = searches
{ searchRepo(queryString = it.query) }
.cachedIn(viewModelScope)
state = combine(...)
}
}
在 UI 中消费 PagingData
// 之前
// class ReposAdapter : ListAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
// …
// }
// 之后
class ReposAdapter : PagingDataAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
…
}
view raw
△ 从 ListAdapter 切换到 PagingDataAdapter
接下来,我们开始从 PagingData Flow 中收集数据,我们可以这样使用 submitData 挂起函数将它的发射绑定到 PagingDataAdapter。
private fun ActivitySearchRepositoriesBinding.bindList(
…
pagingData: Flow<PagingData<Repo>>,
) {
…
lifecycleScope.launch {
pagingData.collectLatest(repoAdapter::submitData)
}
}
△ 使用 PagingDataAdapter 消费 PagingData
注意 colletLatest 的使用
private fun ActivitySearchRepositoriesBinding.bindList(
repoAdapter: ReposAdapter,
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
…
) {
…
val notLoading = repoAdapter.loadStateFlow
// 仅当 PagingSource 的 refresh (LoadState 类型) 发生改变时发射
.distinctUntilChangedBy { it.source.refresh }
// 仅响应 refresh 完成,也就是 NotLoading。
.map { it.source.refresh is LoadState.NotLoading }
val hasNotScrolledForCurrentSearch = uiState
.map { it.hasNotScrolledForCurrentSearch }
.distinctUntilChanged()
val shouldScrollToTop = combine(
notLoading,
hasNotScrolledForCurrentSearch,
Boolean::and
)
.distinctUntilChanged()
lifecycleScope.launch {
shouldScrollToTop.collect { shouldScroll ->
if (shouldScroll) list.scrollToPosition(0)
}
}
}
△ 实现有新查询时自动滚动到顶部
添加头部和尾部
Paging 库的另一个优点是在 LoadStateAdapter 的帮助下,能够在页面的顶部或底部显示进度指示器。RecyclerView.Adapter 的这一实现能够在 Pager 加载数据时自动对其进行通知,使其可以根据需要在列表顶部或底部插入项目。
而它的精髓是您甚至不需要改变现有的 PagingDataAdapter。withLoadStateHeaderAndFooter 扩展函数可以很方便地使用头部和尾部包裹您已有的 PagingDataAdapter。
private fun ActivitySearchRepositoriesBinding.bindState(
uiState: StateFlow<UiState>,
pagingData: Flow<PagingData<Repo>>,
uiActions: (UiAction) -> Unit
) {
val repoAdapter = ReposAdapter()
list.adapter = repoAdapter.withLoadStateHeaderAndFooter(
header = ReposLoadStateAdapter { repoAdapter.retry() },
footer = ReposLoadStateAdapter { repoAdapter.retry() }
)
}
后续
使用 PagingDataAdapter 将我们的 Paging 集成到 UI 上
使用 PagingDataAdapter 暴露的 LoadStateFlow 来保证仅当 Pager 结束加载时滚动到列表的顶部
使用 withLoadStateHeaderAndFooter() 实现当获取数据时将加载栏添加到 UI 上
推荐阅读