Explore Paging 3.0: paging loading data from the network and database | MAD Skills

Welcome back MAD Skills Collection Paging 3.0! In the last article< Get data and bind to UI | MAD Skills >In, we integrated the Pager in the ViewModel and filled the UI with data using the PagingDataAdapter. We also added a load status indicator and reloaded it when an error occurred.

This time, we raise the difficulty to a higher level. So far, we load data directly through the network, and this operation is only applicable to the ideal environment. Sometimes we may encounter slow network connection or complete disconnection. At the same time, even if the network is in good condition, we don't want our application to become a data black hole - pulling data when navigating to each interface is a very wasteful behavior.

The solution to this problem is to load data from the local cache and refresh only when necessary. Updates to cached data must first reach the local cache and then propagate to the ViewModel. In this way, the local cache can become the only trusted data source. It is very convenient for us that the Paging library can cope with this scenario with some small help from the Room library. Let's start now! click here See Paging: display data and its loading status video for more details.

Creating PagingSource using Room

Since the data source to be paged will come from the local rather than directly depend on the API, the first thing we need to do is to 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, the help here is far more than one point: just add a declaration for PagingSource in the DAO of Room, and you can get PagingSource through the DAO!

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

We can now update the Pager 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
    }

RemoteMediator

So far everything has gone well... But we seem to have forgotten something. How does the local database fill in data? Let's see RemoteMediator , when the data in the database is loaded, it is responsible for loading more data from the network. Let's see how it works.

The key to understanding RemoteMediator is to recognize that it is a callback. The result of RemoteMediator will never be displayed on the UI, because it is only used by Paging to inform us as developers that the data of PagingSource has been exhausted. It's our job to update the database and notify Paging. Similar to PagingSource, RemoteMediator has two generic parameters: query parameter type and return value type.

@OptIn(ExperimentalPagingApi::class)
class GithubRemoteMediator(
    ...
) : RemoteMediator<Int, Repo>() {
    ...
}

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 loading starts. Its return value is InitializeAction. InitializeAction can be LAUNCH_INITIAL_REFRESH or SKIP_INITIAL_REFRESH. The former means that the load type carried when calling the load() method is refresh, and the latter means that the remote mediator will be used to perform the refresh operation only when the UI explicitly initiates the request. In our use case, since the warehouse status may be updated quite frequently, we return LAUNCH_INITIAL_REFRESH.

  override suspend fun initialize(): InitializeAction {
        return InitializeAction.LAUNCH_INITIAL_REFRESH
    }

Next, let's look at the load method. The load method is called at the boundary defined by loadType and PagingState. The load type can be refresh, append or prepend. This method is responsible for obtaining data, persisting it on disk and notifying the processing result. The result can be Error or Success. If the result is Error, the load status will reflect this result and the load may be retried. If the loading is successful, you need to notify Pager whether more data can be loaded.

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

        val page = when (loadType) {
            LoadType.REFRESH -> ...
            LoadType.PREPEND -> ...
            LoadType.APPEND -> ...
        }

        val apiQuery = query + IN_QUALIFIER

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

            val repos = apiResponse.items
            val endOfPaginationReached = 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)
        }
    }

Since the load method is a pending function with a return value, the UI can accurately reflect the status of loading completion. stay Last article In, we briefly introduced the withLoadStateHeaderAndFooter extension function and learned how to use it to load the head and bottom. We can observe that the name of the extension function contains a type: LoadState. Let's learn more about this type.

LoadStates, LoadStates, and CombinedLoadStates

Since paging is a series of asynchronous events, it is very important to reflect the current state of loaded data through the UI. In the paging operation, the loading state of the Pager is represented by the CombinedLoadStates type.

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

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

  • Loading
  • NotLoading
  • Error

LoadStates is a data class that contains the following three LoadState values:

  • append
  • prepend
  • refresh

Generally speaking, the prepend and append loading states are used to respond to additional data acquisition, while the refresh loading state is used to respond to initial loading, refresh and retry.

Because Pager may load data from PagingSource or RemoteMediator, combined loadstates 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. They also contain refresh, append and prepend fields, which reflect the LoadState of RemoteMediator or PagingSource based on the configuration and other semantics of Paging. Be sure to check the relevant documents to determine the behavior of these fields in different scenarios.

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

lifecycleScope.launch {
    repoAdapter.loadStateFlow.collect { loadState ->
        // When a refresh error occurs, the retry header is displayed, and the status of the previous cache or the default prepend status is displayed
        header.loadState = loadState.mediator
            ?.refresh
            ?.takeIf { it is LoadState.Error && repoAdapter.itemCount > 0 }
            ?: loadState.prepend

        val isListEmpty = loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
        // Show empty list
        emptyList.isVisible = isListEmpty
        // Whether the data comes from the local database or remote data, the list is displayed only when the refresh is successful.
        list.isVisible =  loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
        // Show load indicator on initial load or refresh
        progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
        // If the initial load or refresh fails, the retry status is displayed
        retryButton.isVisible = loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0
    }
}

We start collecting data from Flow and use combinedloadstates. When Pager has not been loaded and the existing list is empty The refresh field shows the progress bar. We use the refresh field because we only want to display the large progress bar when the application is started for the first time or when the refresh is explicitly triggered. We can also check whether there are load status errors and notify the user.

review

In this article, we implement the following functions:

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

Thank you for reading. The next article will be This series Please look forward to the last article of.

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!

Keywords: kotlin

Added by sandrol76 on Sun, 19 Dec 2021 04:57:22 +0200