paging3有3种使用方式。
- 从数据库加载页面
- 从网络加载页面
- 从网络和数据库加载页面
1.从数据库加载页面
首先,模拟生成数据。
// ArticleViewModel
private val articleList = (0..500).map {
Article(
//id = it,
id = 0,
title = "Article $it",
description = "This describes article $it",
// minusDays(int n) 生成当前日期之前 n 天的日期
created = firstArticleCreatedTime.minusDays(it.toLong())
)
}
fun addLocalList() {
viewModelScope.launch {
withContext(Dispatchers.Default){
//处理耗时操作
repository.addAllList(
articleList
)
}
}
}
然后通过 room 获取 PagingData
// MainActivity
val items = articleViewModel.allArticlePagingData
// ArticleViewModel
val allArticlePagingData: Flow<PagingData<Article>> = Pager(
config = PagingConfig(
pageSize = 60,
enablePlaceholders = true,
maxSize = 200
),
// 通过 room 获取 PagingSource
pagingSourceFactory = { repository.getAllArticleById() }
)
.flow
.cachedIn(viewModelScope)
// ArticleDao
@Query("SELECT * FROM Article ORDER BY id COLLATE NOCASE ASC")
fun getAllArticleById(): PagingSource<Int, Article>
然后调用终止操作符collectLatest。
// MainActivity
lifecycleScope.launch {
// We repeat on the STARTED lifecycle because an Activity may be PAUSED
// but still visible on the screen, for example in a multi window app
repeatOnLifecycle(Lifecycle.State.STARTED) {
items.collectLatest {
articleAdapter.submitData(it)
}
}
}
这种不用自定义PagingSource。
2.从网络加载页面
自定义PagingSource。
// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
private const val GITHUB_STARTING_PAGE_INDEX = 1
private const val TAG = "GithubPagingSource"
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
Log.d(TAG,"inner load")
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
return try {
val response = service.searchRepos(apiQuery, position, params.loadSize)
val repos = response.items
val nextKey = if (repos.isEmpty()) {
null
} else {
// initial load size = 3 * NETWORK_PAGE_SIZE
// ensure we're not requesting duplicating items, at the 2nd request
// 初次加载 params.loadSize 为 NETWORK_PAGE_SIZE 3倍数量
// position = 1 params.loadSize = 90 NETWORK_PAGE_SIZE = 30
// 后续 paging 加载 params.loadSize 为 NETWORK_PAGE_SIZE 数量
// position = 5 params.loadSize = 30 NETWORK_PAGE_SIZE = 30
// position = 4 params.loadSize = 30 NETWORK_PAGE_SIZE = 30
Log.d(TAG,"position = $position params.loadSize = ${params.loadSize}" +
" NETWORK_PAGE_SIZE = $NETWORK_PAGE_SIZE")
position + (params.loadSize / NETWORK_PAGE_SIZE)
}
//初次加载 nextKey = 4 position = 1
//第二次 load nextKey = 5 position = 4
//第三次 load nextKey = 6 position = 5
Log.d(TAG,"nextKey = $nextKey position = $position")
LoadResult.Page(
data = repos,
prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
nextKey = nextKey
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
// The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
// We need to get the previous key (or next key if previous is null) of the page
// that was closest to the most recently accessed index.
// Anchor position is the most recently accessed index
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
返回Flow<PagingData<Repo>>数据。
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false
),
pagingSourceFactory = { GithubPagingSource(service, query) }
).flow
}
3.从网络和数据库加载页面
https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=zh-cn
确保您的应用在网络连接不稳定或用户离线时也可以正常使用,从而提供更好的用户体验。一种方式是同时从网络和本地数据库加载页面。这样,您的应用就会从本地数据库缓存驱动界面,并且仅在数据库中不再有数据时才向网络发出请求。
// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
private const val GITHUB_STARTING_PAGE_INDEX = 1
const val IN_QUALIFIER = "in:name,description"
@OptIn(ExperimentalPagingApi::class)
class GithubRemoteMediator(
private val query: String,
private val service: GithubApi,
private val repoDatabase: RepoDatabase
) : RemoteMediator<Int, Repo>() {
override suspend fun initialize(): InitializeAction {
// Launch remote refresh as soon as paging starts and do not trigger remote prepend or
// append until refresh has succeeded. In cases where we don't mind showing out-of-date,
// cached offline data, we can return SKIP_INITIAL_REFRESH instead to prevent paging
// triggering remote refresh.
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: GITHUB_STARTING_PAGE_INDEX
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
// If remoteKeys is null, that means the refresh result is not in the database yet.
// We can return Success with `endOfPaginationReached = false` because Paging
// will call this method again if RemoteKeys becomes non-null.
// If remoteKeys is NOT NULL but its prevKey is null, that means we've reached
// the end of pagination for prepend.
val prevKey = remoteKeys?.prevKey
if (prevKey == null) {
return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
prevKey
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
// If remoteKeys is null, that means the refresh result is not in the database yet.
// We can return Success with `endOfPaginationReached = false` because Paging
// will call this method again if RemoteKeys becomes non-null.
// If remoteKeys is NOT NULL but its nextKey is null, that means we've reached
// the end of pagination for append.
val nextKey = remoteKeys?.nextKey
if (nextKey == null) {
return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
nextKey
}
}
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 {
// clear all tables in the database
if (loadType == LoadType.REFRESH) {
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
}
val prevKey = if (page == GITHUB_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = repos.map {
RemoteKeys(repoId = it.id, prevKey = prevKey, nextKey = nextKey)
}
repoDatabase.remoteKeysDao().insertAll(keys)
repoDatabase.reposDao().insertAll(repos)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Repo>): RemoteKeys? {
// Get the last page that was retrieved, that contained items.
// From that last page, get the last item
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { repo ->
// Get the remote keys of the last item retrieved
repoDatabase.remoteKeysDao().remoteKeysRepoId(repo.id)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Repo>): RemoteKeys? {
// Get the first page that was retrieved, that contained items.
// From that first page, get the first item
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { repo ->
// Get the remote keys of the first items retrieved
repoDatabase.remoteKeysDao().remoteKeysRepoId(repo.id)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Repo>
): RemoteKeys? {
// The paging library is trying to load data after the anchor position
// Get the item closest to the anchor position
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.id?.let { repoId ->
repoDatabase.remoteKeysDao().remoteKeysRepoId(repoId)
}
}
}
}
使用GithubApi和RepoDatabase,创建remoteMediator和pagingSourceFactory,用于构建Pager。
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
Log.d("GithubRepository", "New query: $query")
// appending '%' so we can allow other characters to be before and after the query string
val dbQuery = "%${query.replace(' ', '%')}%"
val pagingSourceFactory = {
database.reposDao().reposByName(dbQuery)
}
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false),
remoteMediator = GithubRemoteMediator(
query,
githubApi,
database
),
pagingSourceFactory = pagingSourceFactory
).flow
}
然后调用getRepo()
。
// GithubViewModel
// 获取 Repo
fun getRepo(): Flow<PagingData<Repo>> {
return repository.getSearchResultStream("Android").cachedIn(viewModelScope)
}
// MainActivity
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 获取 PagingData
githubViewModel.getRepo()
.catch {
Log.d(TAG,"Exception : ${it.message}")
}
.collectLatest {
Log.d(TAG,"inner collectLatest")
// paging 使用 submitData 填充 adapter
repoAdapter.submitData(it)
}
}
}
代码是在的paging基础上进行删减,尽量达到最少代码。
更多具体教程可查看对应codelabs。
https://developer.android.com/codelabs/android-paging?hl=zh-cn#13
https://developer.android.com/codelabs/android-paging-basics#0
本文代码地址:
https://github.com/VIVILL/SimpleDemo/tree/main/Paging3
关于paging3 局部更新
例如,登录账户后,收藏文章后,需要更新收藏文章状态。
需重写DiffUtil.ItemCallback中的getChangePayload。
override fun getChangePayload(oldItem: Article, newItem: Article): Any? {
val bundle = Bundle()
// onBindViewHolder 实现三个参数 payloads 第一个数据为 封装的bundle
Log.d(TAG,"inner getChangePayload collect = ${oldItem.collect} " +
"collect = ${newItem.collect} ")
if (oldItem.collect != newItem.collect){
bundle.putBoolean("collect",newItem.collect)
}
return bundle
}
然后在onBindViewHolder中获取数据后更新UI。
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int, payloads: List<Any>) {
Log.d(TAG,"inner onBindViewHolder payloads")
if (payloads.isEmpty()) {
Log.d(TAG,"payloads isEmpty")
// payloads为空,说明是更新整个ViewHolder
onBindViewHolder(holder, position);
} else {
// payloads不为空,这只更新需要更新的View即可。
val payload: Bundle = payloads[0] as Bundle
Log.d(TAG,"payload = $payload")
holder.updateImageView(payload.getBoolean("collect"))
}
}
在执行收藏或取消收藏后,监听action执行结果,调用Adapter.refresh(),就可以更新了,无需position,一般的recyclerview是调用 Adapter.notifyItemChanged(it.position,true)
。使用paging的话可以直接调用Adapter.refresh()。
articleAdapter.setImageViewClickListener { id,collect->
// 如果是已收藏状态 就取消收藏 如果是未收藏状态则 收藏
Log.d(TAG,"inner setImageViewClickListener collect = $collect")
if (collect){
collectViewModel.unCollect(id)
}else {
collectViewModel.collect(id)
}
}
viewLifecycleOwner.lifecycleScope.launch(exceptionHandler) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
collectViewModel.collectAction.collect {
when (it) {
is CollectAction.Success -> {
Log.d(TAG,"CollectAction.Success")
articleAdapter.refresh()
}
is CollectAction.Error -> {
articleAdapter.refresh()
}
else -> {}
}
}
}
}
具体可参考这篇:
https://github.com/VIVILL/JetpackDemo
remove的话,也可以调用adapter.notifyItemRemoved(position)。
参考链接:
Paging 库概览
https://developer.android.com/codelabs/android-paging?hl=zh-cn#13
https://developer.android.com/codelabs/android-paging-basics#0
https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=zh-cn
https://developer.android.com/training/data-storage/room/referencing-data
https://stackoverflow.com/questions/50515820/android-room-error-cannot-figure-out-how-to-save-this-field-into-database
https://github.com/android/architecture-components-samples/tree/master/PagingSample
网友评论