Welcome back to Paging 3.0 of MAD Skills series In the last article Introduction to Paging 3.0 , we discussed the Paging library and learned how to integrate it into the application architecture and integrate it into the data layer of the application. We use PagingSource
to obtain and use data for our application, and use PagingConfig
Pager
objects that can provide Flow<PagingData>
for UI consumption. In this article I will introduce how to actually use Flow<PagingData>
in your UI.
Prepare PagingData for UI
The application of the existing ViewModel
UiState
data class that can provide the information needed to render the UI. It contains a searchResult
field, which is used to cache the search results in the memory and provide data after configuration changes.
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()
}
△ Initial UiState definition
Now access Paging 3.0, we removed UiState
the searchResult
, and select UiState
exposing a separate addition PagingData<Repo>
of Flow
instead. This new Flow
function searchResult
the same: to provide a list of items to make UI rendering.
ViewModel
added a private "searchRepo()"
method, which calls Repository
to provide Pager
in PagingData Flow
. Flow<PagingData<Repo>>
based on the search term entered by the user. We also used the cachedIn
operator on the generated PagingData Flow
, so that it can be quickly reused ViewModelScope
class SearchRepositoriesViewModel(
private val repository: GithubRepository,
…
) : ViewModel() {
…
private fun searchRepo(queryString: String): Flow<PagingData<Repo>> =
repository.getSearchResultStream(queryString)
}
△ Integrated PagingData Flow for the warehouse
exposes a PagingData Flow independent of other Flows. Because PagingData
itself is a variable type, it maintains its own data stream internally and will update with time.
With all the Flow
that make up the UiState
field defined, we can combine it into UiState
of StateFlow
, and expose it for UI consumption together with PagingData
of Flow
After completing this, we can now start to consume our Flow
in the UI.
class SearchRepositoriesViewModel(
…
) : ViewModel() {
val state: StateFlow<UiState>
val pagingDataFlow: Flow<PagingData<Repo>>
init {
…
pagingDataFlow = searches
.flatMapLatest { searchRepo(queryString = it.query) }
.cachedIn(viewModelScope)
state = combine(...)
}
}
△ Expose PagingData Flow to UI. Pay attention to the use of cachedIn operator
in the UI
The first thing we need to do is switch RecyclerView Adapter
from ListAdapter to PagingDataAdapter
. PagingDataAdapter
for comparison PagingData
difference and polymerizing updated optimized RecyclerView Adapter
, change the background to ensure that the data set can be transferred as efficiently as possible.
// 之前
// class ReposAdapter : ListAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
// …
// }
// 之后
class ReposAdapter : PagingDataAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
…
}
view raw
△ Switch from ListAdapter to PagingDataAdapter
Next, we start PagingData Flow
, we can use the submitData
suspend function to bind its emission to PagingDataAdapter
.
private fun ActivitySearchRepositoriesBinding.bindList(
…
pagingData: Flow<PagingData<Repo>>,
) {
…
lifecycleScope.launch {
pagingData.collectLatest(repoAdapter::submitData)
}
}
△ Use PagingDataAdapter to consume PagingData Pay attention to the use of colletLatest
In addition, for the sake of user experience, we want to ensure that when users search for new content, they will return to the top list to display the first search result. We expect to do this when and display the data to UI We use PagingDataAdapter
exposed loadStateFlow
and UiState
in "hasNotScrolledForCurrentSearch"
to track whether the user to manually scroll through the list field. Combining the two can create a marker to let us know whether or not automatic scrolling should be triggered.
Since loadStateFlow
is synchronized with the content displayed in the UI, we can NotLoading
scroll to the top of the list loadStateFlow
informs us that a new query is in the 061864b22a3e1c state.
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)
}
}
}
△ Automatically scroll to the top when there is a new query
add head and tail
Another advantage of the Paging library is that with LoadStateAdapter
, the progress indicator can be displayed at the top or bottom of the page. RecyclerView.Adapter
this can be achieved in Pager
automatically be notified when data is loaded, it may need to insert a list of the top or bottom of the project according to.
And its essence is that you don’t even need to change the existing PagingDataAdapter
. withLoadStateHeaderAndFooter
extension function can conveniently use the head and tail to wrap your existing 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() }
)
}
△ head and tail
withLoadStateHeaderAndFooter
LoadStateAdapter
for the head and tail. These LoadStateAdapter
correspondingly host their own ViewHolder
, these ViewHolder
to the latest loading state, so it is easy to define the view behavior. We can also pass in parameters to retry the loading when an error occurs, which I will introduce in detail in the next article.
follow up
We have bound PagingData to the UI! Let's quickly recap:
- Use PagingDataAdapter to integrate our Paging into the UI
- Use LoadStateFlow exposed by PagingDataAdapter to ensure that only when Pager finishes loading, scroll to the top of the list
- Use withLoadStateHeaderAndFooter() to add the loading bar to the UI when data is obtained
Thank you for reading! Stay tuned for the next article, we will explore the use of Paging to implement the database as a single source, and discuss LoadStateFlow
detail!
Welcome to Click here to submit feedback to us, or share your favorite content or problems found. Your feedback is very important to us, thank you for your support!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。