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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。