头图

Welcome back to Paging 3.0 of MAD Skills series In the previous article " Get Data and Bind to UI | MAD Skills ", we integrated ViewModel Pager , and used the matching PagingDataAdapter to fill the UI with data, and we also added a loading status indicator when it appeared. Reload on error.

This time, we raised the difficulty to a level. So far, we have loaded data directly through the network, and this operation is only suitable for ideal environments. Sometimes we may encounter a slow network connection or a 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 local cache and refresh it only when necessary. Updates to the 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. What is very convenient for us is that the Paging library can handle this scenario with some small help from the Room library. Let's get started now! Click here view Paging: Display data and its loading status video for more details.

Use Room to create PagingSource

Since the data source we are going to page will come from the local instead of directly relying on the API, the first thing we need to do is to update 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 much more than a little help here: just as in the Room of the DAO PagingSource added the statement, you can by DAO get PagingSource !

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

We can now update GitHubRepository Pager 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 is going well... but we seem to have forgotten something. How to fill the local database with data? Take a look at RemoteMediator . When the data in the database is loaded, it is responsible for loading more data from the network. Let us see how it works.

The key to understanding RemoteMediator is to realize that it is a callback. RemoteMediator will never be displayed on the UI, because it is only used by Paging to notify us as a developer that the data of PagingSource has been exhausted. Update the database and notify Paging, this is our own job. Similar to PagingSource RemoteMediator has two generic parameters: query parameter type and return value type.

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

Let us take a closer look at the abstract methods in RemoteMediator The first method is initialize() RemoteMediator before all loading starts, and 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 RemoteMediator 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 we look at the load method. load method is called at the boundary defined by loadType and PagingState refresh , append or prepend . This method is responsible for obtaining data, persisting it on the disk and notifying the processing result. The result can be Error or Success . If the result is Error, the loading status will reflect this result and the loading 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 suspended function with a return value, the UI can accurately reflect the status of the loading completion. In the previous article , 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 us learn more about this type.

LoadState, LoadStates, and CombinedLoadStates

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

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

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

  • Loading
  • NotLoading
  • Error

LoadStates is a data class containing the following three types of LoadState

  • append
  • prepend
  • refresh

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

Since Pager may load data PagingSource or RemoteMediator CombinedLoadStates has two LoadState fields. The field named source is used for PagingSource , and mediator is used for RemoteMediator .

Convenience, CombinedLoadStates and LoadStates similar, also contains refresh , append and prepend fields, they based Paging reflect the configuration and other semantic RemoteMediator or PagingSource of LoadState . Be sure to check the relevant documentation to determine the behavior of these fields in different scenarios.

Using this information to update our UI is as simple as getting data PagingAdapter exposed by loadStateFlow In our application, we can use this information to display a loading indicator when it is first loaded:

lifecycleScope.launch {
    repoAdapter.loadStateFlow.collect { loadState ->
        // 在刷新出错时显示重试头部,并且展示之前缓存的状态或者展示默认的 prepend 状态
        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
        // 显示空列表
        emptyList.isVisible = isListEmpty
        // 无论数据来自本地数据库还是远程数据,仅在刷新成功时显示列表。
        list.isVisible =  loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
        // 在初始加载或刷新时显示加载指示器
        progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
        // 如果初始加载或刷新失败,显示重试状态
        retryButton.isVisible = loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0
    }
}

We started Flow , and when Pager has not been loaded and the existing list is empty, we use the CombinedLoadStates.refresh field to display the progress bar. We use the refresh field because we only want to display the large progress bar when the application is launched for the first time or when a refresh is explicitly triggered. We can also check whether there is an error in the loading status and notify the user.

review

In this article, we have implemented the following functions:

  • Use the database as the only trusted data source, and page the data;
  • Use RemoteMediator to fill PagingSource based on Room;
  • Use LoadStateFlow from PagingAdapter to update the UI with a progress bar.

Thank you for reading. The next article will be this series of , so stay tuned.

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 开发者文档。