Get data and bind to UI | MAD Skills

Keywords: Android kotlin

Welcome back MAD Skills Collection Paging 3.0! In the last article Paging 3.0 introduction In the 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 use PagingSource to obtain and use data for our application, and PagingConfig to create Pager objects that can provide flow < pagingdata > for UI consumption. In this article, I'll show you how to actually use flow < pagingdata > in your UI.

Preparing PagingData for UI

Applying the existing ViewModel exposes the UiState data class that can provide the information required for rendering the UI. It contains a searchResult field to cache the search results in 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 remove the searchResult in UiState and choose to expose a pagingdata < repo > Flow outside UiState instead. This new Flow function is the same as searchResult: it provides a list of items for UI rendering.

A private "searchRepo()" method is added to ViewModel, which calls Repository to provide PagingData Flow in Pager. We can call this method to create flow < pagingdata < repo > > based on the search term entered by the user. We also used the cached in operator on the generated PagingData Flow to enable it to be quickly reused through ViewModelScope.

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

△ integrating PagingData Flow for warehouse

It is important to expose a PagingData Flow that is independent of other flows. Because PagingData itself is a variable type, it internally maintains its own data Flow and will be updated over time.

As all the flows that make up the UiState field are defined, we can combine them into the StateFlow of UiState and expose them to UI consumption together with the Flow of PagingData. After that, 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(...)
    }

}

△ expose PagingData Flow to the UI. Pay attention to the use of cached in operator

Consuming PagingData in UI

The first thing we need to do is switch the RecyclerView Adapter from ListAdapter to PagingDataAdapter. PagingDataAdapter is a RecyclerView Adapter optimized to compare PagingData differences and aggregate 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 raw

△ switch from ListAdapter to PagingDataAdapter

Next, we start to collect data from 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 the PagingDataAdapter to consume PagingData. Pay attention to the use of colletlast

In addition, for the sake of the user experience, we want to ensure that when users search for new content, they will go back to the top of the list to display the first search result. We expect to do this when we have finished loading and have presented the data to the UI. We track whether users manually scroll the list by using the "hasNotScrolledForCurrentSearch" field in loadStateFlow and UiState exposed by PagingDataAdapter. Combining the two can create a tag to let us know whether automatic scrolling should be triggered.

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

private fun ActivitySearchRepositoriesBinding.bindList(
        repoAdapter: ReposAdapter,
        uiState: StateFlow<UiState>,
        pagingData: Flow<PagingData<Repo>>,
        ...
    ) {
        ...
        val notLoading = repoAdapter.loadStateFlow
            // Emit only when the refresh (LoadState type) of PagingSource changes
            .distinctUntilChangedBy { it.source.refresh }
            // Only respond to refresh completion, that is, 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 the ability to display progress indicators at the top or bottom of the page with the help of the LoadStateAdapter. This implementation of RecyclerView.Adapter can automatically notify the Pager when it loads data, so that it can insert items at the top or bottom of the list as needed.

The essence of it is that you don't even need to change the existing paging data adapter. The withLoadStateHeaderAndFooter extension function can easily wrap your existing PagingDataAdapter with the head and tail.

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

LoadStateAdapter is defined for both header and tail in the parameters of withLoadStateHeaderAndFooter function. These loadstateadapters correspondingly host their own viewholders, which are bound to the latest load state, so it is easy to define view behavior. We can also pass in parameters to retry loading when an error occurs, which will be described in detail in the next article.

follow-up

We have bound PagingData to the UI! Let's quickly review:

  • Using PagingDataAdapter to integrate our Paging into the UI
  • Use the LoadStateFlow exposed by the PagingDataAdapter to ensure that you scroll to the top of the list only when the Pager ends loading
  • Use withLoadStateHeaderAndFooter() to add the load bar to the UI when getting data

Thank you for reading! Please pay attention to the next article. We will discuss using Paging to implement database as a single source, and discuss LoadStateFlow in detail!

Welcome click here Submit feedback to us, or share your favorite content and found problems. Your feedback is very important to us. Thank you for your support!

Posted by basdog22 on Sun, 31 Oct 2021 21:12:12 -0700