美文网首页
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