Welcome back to Paging 3.0 in the MAD Skills series! In an article on “to get the data and bind to the UI | MAD Skills”, we integrated the Pager in the ViewModel, and use the filling data to the UI PagingDataAdapter, we also add the loading status indicator, and reload when there is an error.

This time, we’re taking it up a notch. So far, we’ve loaded data directly from the network, which is only ideal. We may sometimes experience slow Internet connections or complete disconnection. At the same time, even if the network is in good shape, we don’t want our apps to be data black holes — pulling data every time we navigate to every interface is wasteful.

The solution to this problem is to load the data from the local cache and refresh it only when necessary. Updates to cached data must reach the local cache before being propagated to the ViewModel. This makes the local cache the only trusted data source. It is convenient for us that the Paging library can handle this scenario with a little help from the Room library. Let’s get started! Click here to view Paging: Display data and its loading status video for more details.

Create the PagingSource using Room

Since the data sources we will be paging will be local rather than relying directly on the API, the first thing we need to do is update the PagingSource. The good news is that we have very little work to do. Is it because of the “little help from Room” I mentioned earlier? In fact, there’s more to it than that: just add a declaration for PagingSource in the Room DAO to get the PagingSource from the DAO!

@Dao
interface RepoDao {
    @Query(
        "SELECT * FROM repos WHERE " +
            "name LIKE :queryString"
    )
    fun reposByName(queryString: String): PagingSource<Int, Repo>
}
Copy the code

We can now update Pager’s constructor in GitHubRepository to use the new PagingSource:

fun getSearchResultStream(query: String): Flow < PagingData < Repo > > {...val pagingSourceFactory = { database.reposDao().reposByName(dbQuery) }

        @OptIn(ExperimentalPagingApi::class)
        return Pager(
           config = PagingConfig(
                pageSize = NETWORK_PAGE_SIZE,
                enablePlaceholders = false), pagingSourceFactory = pagingSourceFactory, remoteMediator =... , ).flow }Copy the code

RemoteMediator

So far so good… But I think we forgot something. How does the local database populate the data? Take a look at RemoteMediator, which is responsible for loading more data from the network when the data in the database has finished loading. Let’s see how it works.

The key to understanding RemoteMediator is to recognize that it is a callback. The result of RemoteMediator is never displayed on the UI because it is simply Paging to inform us as developers that the PagingSource has run out of data. It is our job to update the database and notify Paging. Like PagingSource, RemoteMediator takes two generic parameters: the query parameter type and the return value type.

@OptIn(ExperimentalPagingApi::class)
class GithubRemoteMediator(...). : RemoteMediator<IntThe Repo > () {... }Copy the code

Let’s take a closer look at the abstract methods in RemoteMediator. The first method is Initialize (), which is the first method called by RemoteMediator before all loads begin and returns an InitializeAction. InitializeAction can be LAUNCH_INITIAL_REFRESH or SKIP_INITIAL_REFRESH. The former means that the load type is refresh when the load() method is called, while the latter means that RemoteMediator is only used to refresh when the UI explicitly initiates the request. In our use case, we returned to LAUNCH_INITIAL_REFRESH because the warehouse state might be updated quite frequently.

  override suspend fun initialize(a): InitializeAction {
        return InitializeAction.LAUNCH_INITIAL_REFRESH
    }
Copy the code

So let’s look at the load method. The load method is called at the boundary defined by loadType and PagingState, and the load type can be Refresh, Append, or prepend. This method is responsible for getting the data, persisting it on disk and notifying the processing result, which can be Error or Success. If the result is Error, the load status will reflect the result and the load may be retried. If the load succeeds, the Pager needs to be notified if more data can be loaded.

override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult {

        val page = when(loadType) {loadtype. REFRESH ->... LoadType. The PREPEND - >... LoadType. APPEND - >... }val apiQuery = query + IN_QUALIFIER

        try {
            val apiResponse = service.searchRepos(apiQuery, page, state.config.pageSize)

            val repos = apiResponse.items
            valEndOfPaginationReached = repos. IsEmpty () repoDatabase. WithTransaction {... repoDatabase.reposDao().insertAll(repos) }return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
        } catch (exception: IOException) {
            return MediatorResult.Error(exception)
        } catch (exception: HttpException) {
            return MediatorResult.Error(exception)
        }
    }
Copy the code

Since the load method is a suspended function with a return value, the UI accurately reflects the state of completion of the load. In the previous article, we briefly introduced the withLoadStateHeaderAndFooter extension function, and know how to use it to load the head and the bottom. As you can see, the extension function has a type in its name: LoadState. Let’s take a closer look at this genre.

LoadState, LoadStates, and CombinedLoadStates

Because paging is a series of asynchronous events, it is important to reflect the current state of the loaded data through the UI. In paging, the load status of a Pager is represented by the CombinedLoadStates type.

As the name implies, this type is a combination of other types that represent loaded information. These types include:

LoadState is a sealed class that completely describes the following load states:

  • Loading
  • NotLoading
  • Error

LoadStates is a data class that contains three LoadState values:

  • append
  • prepend
  • refresh

In general, the prepend and Append load states are used in response to additional data fetching, while the refresh load state is used in response to initial load, refresh, and retry.

Since Pager may load data from PagingSource or RemoteMediator, CombinedLoadStates has two LoadState fields. The field named Source is used for PagingSource and the field named Mediator is used for RemoteMediator.

For convenience, CombinedLoadStates is similar to LoadStates in that it also contains refresh, Append, and prepend fields, They reflect the LoadState of RemoteMediator or PagingSource based on the configuration of Paging and other semantics. Be sure to review the documentation to determine how these fields behave in different scenarios.

Updating our UI with this information is as simple as retrieving data from loadStateFlow exposed by the PagingAdapter. In our application, we can use this information to display a load indicator on the first load:

lifecycleScope.launch {
    repoAdapter.loadStateFlow.collect { loadState ->
        // Displays the retry header when a refresh error occurs and displays the state of the previous cache or the default prepend stateheader.loadState = loadState.mediator ? .refresh ? .takeIf { itis LoadState.Error && repoAdapter.itemCount > 0 }
            ?: loadState.prepend

        val isListEmpty = loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
        // Displays an empty list
        emptyList.isVisible = isListEmpty
        // Display the list only if the refresh is successful, whether the data is from a local database or remote data.
        list.isVisible =  loadState.source.refresh isLoadState.NotLoading || loadState.mediator? .refreshis LoadState.NotLoading
        // Display load indicator on initial load or refreshprogressBar.isVisible = loadState.mediator? .refreshis LoadState.Loading
        // If the initial load or refresh fails, retry status is displayedretryButton.isVisible = loadState.mediator? .refreshis LoadState.Error && repoAdapter.itemCount == 0}}Copy the code

We started collecting data from the Flow, and in the Pager has not yet been loaded and the existing list is empty, use CombinedLoadStates. Refresh fields show the progress bar. We use the Refresh field because we only want the large progress bar to be displayed when the application is first launched or when a refresh is explicitly triggered. We can also check for load status errors and notify the user.

review

In this article, we implemented the following functions:

  • Use the database as the only trusted data source and paginate the data;
  • Populate the Room-based PagingSource with RemoteMediator;
  • Update the UI with the progress bar using LoadStateFlow from the PagingAdapter.

Thanks for reading, and the next article will be the last in this series, so stay tuned.