美文网首页
Paging3简单使用

Paging3简单使用

作者: 梧叶已秋声 | 来源:发表于2022-11-14 20:30 被阅读0次

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 局部更新
例如,登录账户后,收藏文章后,需要更新收藏文章状态。

image.png
需重写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

相关文章

网友评论

      本文标题:Paging3简单使用

      本文链接:https://www.haomeiwen.com/subject/wcvjxdtx.html