Welcome back to Paging 3.0 in the MAD Skills series! In the previous introduction to Paging 3.0 article, we discussed the Paging library, learned how to integrate it into the application architecture and integrate it into the data layer of the application. We used PagingSource to get and use data for our application, and PagingConfig to create Pager objects that provide Flow for UI consumption. In this article I’ll show you how to actually use Flow in your UI.

Prepare PagingData for the UI

Applying the existing ViewModel exposes the UiState data class that provides the information needed to render the UI, which contains a searchResult field that caches the search results in memory and provides the 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()
}
Copy the code

△ Initial UiState definition

Now accessing Paging 3.0, we remove the searchResult in UiState and choose to replace it with a Flow that exposes PagingData

separately from UiState. This new Flow function is the same as searchResult: it provides a list of items for the UI to render.

A private “searchRepo()” method has been added to the ViewModel, which calls Repository to provide PagingData Flow in Pager. We can call this method to create Flow > based on the search terms entered by the user. We also use the cachedIn operator on the generated PagingData Flow to enable rapid reuse through ViewModelScope.

class SearchRepositoriesViewModel(
    private valThe repository: GithubRepository,...). : the ViewModel () {...private fun searchRepo(queryString: String): Flow<PagingData<Repo>> =
        repository.getSearchResultStream(queryString)
}
Copy the code

Integrate PagingData Flow for warehouse

It is important to expose a PagingData Flow that is independent of other flows. Because PagingData is itself a mutable type, it maintains its own data flow internally and updates over time.

With the Flow that makes up the UiState field all defined, we can compose it into the StateFlow of UiState and expose it for UI consumption along with the Flow of PagingData. With this done, we can now start consuming 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(...)
    }

}
Copy the code

Expose PagingData Flow to the UI to note the use of the cachedIn operator

Consuming PagingData in the UI

The first thing we need to do is switch the RecyclerView Adapter from the ListAdapter to the PagingDataAdapter. PagingDataAdapter is a RecyclerView Adapter optimized for comparing the differences of PagingData and aggregating updates to ensure that changes in background data sets can be transmitted as efficiently as possible.

/ / before
// class ReposAdapter : ListAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {
/ /...
// }

/ / after
class ReposAdapter : PagingDataAdapter<Repo, RepoViewHolder>(REPO_COMPARATOR) {... } view rawCopy the code

△ Switch from ListAdapter to PagingDataAdapter

Next, we start collecting data from the PagingData Flow, which we can bind to the PagingDataAdapter by using the submitData suspend function.

private fun ActivitySearchRepositoriesBinding.bindList(... pagingData:Flow<PagingData<Repo>>,
    ){... lifecycleScope.launch { pagingData.collectLatest(repoAdapter::submitData) } }Copy the code

△ Use PagingDataAdapter to consume PagingData note the use of colletLatest

Also, for the sake of user experience, we want to make sure that when users search for new content, they go back to the top of the list to show the first result. We expect to do this when we are finished loading and have presented the data to the UI. We use PagingDataAdapter “hasNotScrolledForCurrentSearch” field in the exposed loadStateFlow and UiState to track whether the user manual scrolling list. Combining the two can create a tag that lets us know if automatic scrolling should be triggered.

Since the loading state provided by loadStateFlow is synchronized with what the UI displays, we can safely scroll to the top of the list every time loadStateFlow informs us that a new query is in a NotLoading state.

private fun ActivitySearchRepositoriesBinding.bindList(
        repoAdapter: ReposAdapter,
        uiState: StateFlow<UiState>,
        pagingData: Flow<PagingData<Repo> >,...).{...val notLoading = repoAdapter.loadStateFlow
            // Fires only when refresh (LoadState type) of PagingSource changes
            .distinctUntilChangedBy { it.source.refresh }
            // Only respond to refresh completion, i.e., 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)}}}Copy the code

△ Automatically scroll to the top when there is a new query

Add a header and a tail

Another advantage of a Paging library is the ability to display progress indicators at the top or bottom of the page with the help of a LoadStateAdapter. This implementation of RecyclerView.Adapter can automatically notify the Pager when it loads data, allowing it to insert items at the top or bottom of the list as needed.

The essence of this is that you don’t even need to change the existing PagingDataAdapter. WithLoadStateHeaderAndFooter extension functions can be easily use the head and tail up 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() }
        )
    }
Copy the code

△ Head and tail

WithLoadStateHeaderAndFooter function parameter defines LoadStateAdapter for the head and tail. These LoadStateAdapters, in turn, host their own ViewHolder, which is bound to the latest load state, making it easy to define view behavior. We can also pass in parameters to retry loading when an error occurs, which I’ll cover in more detail in the next article.

subsequent

We’ve tied PagingData to the UI! A quick recap:

  • Integrate our Paging into the UI using PagingDataAdapter
  • Use the LoadStateFlow exposed by PagingDataAdapter to ensure that you scroll to the top of the list only when Pager finishes loading
  • Using withLoadStateHeaderAndFooter () when get the data will be loaded column is added to the UI

Thank you for reading! Stay tuned for the next article where we explore the implementation of Paging with a database as a single source and LoadStateFlow in detail!

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!