头图

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!


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。