Paging3使用经验分享

前言

paging是Jetpack推出基于 RecyclerView 的 1 个分页库,之前在项目中使用过paging2,使用起来十分复杂,paging3已经出来快一年了,最新版已经到RC版,相信不久后稳定版就会到来。相比paging2,它使用起来简洁了很多,下面就看下如何使用。(本文使用版本为 3.0.0-rc01 ,基于kotlin开发,项目地址 https://github.com/Tommys-code/Jetpack-samples/tree/master/Paging3Sample

Paging的使用

1.依赖:

implementation 'androidx.paging:paging-runtime-ktx:3.0.0-rc01'

2.数据源:

2.1Room

paging3如果使用Room来加载数据,使用非常方便,定义Dao的查询数据时,直接返回 PagingSource 类型。同时也不需要定义页数和每页数量,全部交由Paging控制。

@Dao
interface PersonDao {

    @Query("SELECT * FROM PersonData ORDER BY id desc")
    fun allPerson(): PagingSource<Int, PersonData>

    @Insert
    suspend fun insert(person: List<PersonData>)
    
    @Delete
    suspend fun delete(cheese: PersonData)

}

如果使用Room加载数据,对于插入删除操作,无需额外控制,列表会自动更新。

2.2自定义数据源

对于来自网络或其它地方的数据,需要自定义 PagingSource 

class OtherPagingSource : PagingSource<Int, PersonData>() {

    override fun getRefreshKey(state: PagingState<Int, PersonData>): Int? {
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PersonData> {
        return try {
            //页码未定义置为0
            val currentPage = params.key ?: 0
            //仓库层请求数据
            val demoReqData = OtherRepository.loadData(currentPage)
            //当前页码 小于 总页码 页面加1
            val nextPage = if (!demoReqData.isNullOrEmpty()) {
                currentPage + 1
            } else {
                //没有更多数据
                null
            }
            if (demoReqData != null) {
                LoadResult.Page(data = demoReqData, prevKey = null, nextKey = nextPage)
            } else {
                LoadResult.Error(throwable = Throwable())
            }
        } catch (e: Exception) {
            LoadResult.Error(throwable = e)
        }
    }

}

需要重写2个方法,getRefreshKey 属于比较高级用法,这里使用不到直接返回null就可以了,在 load 方法中写加载数据逻辑就可以了,我在仓库层使用本地数据模拟网络请求,在正式项目中可以替换为网络请求。

3.构建PagingData

val data: Flow<PagingData<PersonData>> = Pager(
        PagingConfig(
            // 每页显示的数据的大小
            pageSize = 60,
            // 开启占位符
            enablePlaceholders = true,
            // 预刷新的距离,距离最后一个 item 多远时加载数据
            prefetchDistance = 3,
            //初始化加载数量,默认为 pageSize * 3
            initialLoadSize = 60,
            //一次应在内存中保存的最大数据,这个数字将会触发,滑动加载更多的数据
            maxSize = 200
        )
    ) {
        //PagingSource
//        OtherPagingSource()
        dao.allPerson()
    }
        .flow
        //cachedIn 绑定协程生命周期
        .cachedIn(viewModelScope)
        //转换成liveData
        //.asLiveData(viewModelScope.coroutineContext)

    

根据数据来源,使用数据库或自定义来源。

4.构建自定义PagingDataAdapter

class PersonAdapter : PagingDataAdapter<PersonData, RecyclerCompatVH<ItemPersonBinding>>(object :
    DiffUtil.ItemCallback<PersonData>() {
    override fun areItemsTheSame(oldItem: PersonData, newItem: PersonData): Boolean =
        oldItem.id == newItem.id

    override fun areContentsTheSame(oldItem: PersonData, newItem: PersonData): Boolean =
        oldItem == newItem

})

直接继承 PagingDataAdapter 传入 DiffUtil.ItemCallback 对象,同时和普通RecycleView.Adapter一样实现2个方法即可。DiffUtil.ItemCallback 是用来对比数据的,一般都是固定写法。

5.绑定数据

构建的数据是 Flow 类型,可以通过 asLiveData 转换为liveData 给adapter绑定,也可以直接用如下方法绑定

lifecycleScope.launchWhenCreated {
     viewModel.data.collect { adapter.submitData(it) }
}

adapter.submitData 是一个协程挂起(suspend)操作,所以要放入协程赋值。到这里数据就已经完成了分页展示,向下滑动会自动加载下一页。

UI状态处理和操作

1.下拉刷新

第一次请求不需要任何操作,订阅数据直接请求;手动刷新直接调用:

adapter.refresh()

2.失败重试

  • 如果加载中间出错需要重试,只需调用:
adapter.retry()

刷新和重试,adapter内部已经封装好了方法,直接调用即可,比paging2方便很多

3.UI状态处理

对于其它状态,需要使用 adapter.addLoadStateListener 添加状态监听,它的参数 CombinedLoadStates 包含 refresh(刷新),prepend(向前加载更多),append(向后加载更多),source(PagingSource加载),mediator(RemoteMediator加载,没有则为null)。

每个行为分为3中状态:

  • LoadState.Loading 加载中 (加载数据时候回调)
  • LoadState.NotLoading 没有加载中 (加载数据前和加载数据完成后回调)
  • LoadState.Error 加载失败 (加载数据失败回调)

下拉刷新处理:

adapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.Error -> {
                    //取消SwipeRefreshLayout刷新
                    binding.refresh.isRefreshing = false
                    //加载出错,显示错误页面
                    binding.multiStateView.viewState = MultiStateView.ViewState.ERROR
                }
                is LoadState.NotLoading -> {
                    binding.refresh.isRefreshing = false
                    //无数据,显示空页面
                    if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached && adapter.itemCount == 0) {
                        binding.multiStateView.viewState = MultiStateView.ViewState.EMPTY
                    } else if (adapter.itemCount > 0){
                        binding.multiStateView.viewState = MultiStateView.ViewState.CONTENT
                    }
                }
            }
        }

这里使用了 MultiStateView 对初始不同状态(空,错误,第一次进入加载)显示不同视图。具体方式可以自行去查看,也可以依据项目要求自行处理逻辑。

上拉加载更多处理:

paging会自动处理加载更多,如果想添加加载更多UI状态显示,PagingDataAdapter 为我们提供了  withLoadStateFooter、withLoadStateHeader以及同时添加头部和尾部方法withLoadStateHeaderAndFooter。对于加载更多,只需自定义 LoadStateAdapter 通过判断 loadstate 来显示不同状态 。

class LoadMoreAdapter(private val retryCallBack: () -> Unit) :
    LoadStateAdapter<RecyclerCompatVH<ItemLoadMoreBinding>>() {

    override fun onBindViewHolder(
        holder: RecyclerCompatVH<ItemLoadMoreBinding>,
        loadState: LoadState
    ) {
        //没有下一页
        holder.binding.noData.visibility =
            if (loadState is LoadState.NotLoading && loadState.endOfPaginationReached) View.VISIBLE else View.GONE
        //加载中
        holder.binding.loding.visibility =
            if (loadState is LoadState.Loading) View.VISIBLE else View.GONE
        //出错,重试按钮
        holder.binding.btnRetry.visibility =
            if (loadState is LoadState.Error) View.VISIBLE else View.GONE

        holder.binding.btnRetry.setOnClickListener {
            retryCallBack.invoke()
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ): RecyclerCompatVH<ItemLoadMoreBinding> {
        return RecyclerCompatVH(
            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context), R.layout.item_load_more, parent, false
            )
        )
    }
    
    override fun displayLoadStateAsItem(loadState: LoadState): Boolean {
        return super.displayLoadStateAsItem(loadState) || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached)
    }

}

这里需要注意,如果想要没有下一页状态,需要重写 displayLoadStateAsItem 方法,因为默认加载完以后会移除当前 item。同时需要将 RecycleView 绑定的 adapter 改为添加header和footer后的adapter:

binding.recycle.adapter = adapter.withLoadStateFooter(LoadMoreAdapter { adapter.retry() })

这是一个 ConcatAdapter(之前叫MergeAdapter) ,它可以按顺序连接其他 Adapter,如果感兴趣可以自己去了解。

对数据的删除、新增

对于来源Room的数据,只需要直接删除或修改Room里的数据源,无需其它操作,UI会自动进行更新。

对于自定义数据源,我们都知道,在之前,我们给adapter设置一个List,如果需要删除或者新增,我们只要改变List即可,但是在Paging3中好像没有办法,因为数据源是 PagingSource ,如果需要在不重新请求情况下进行删除和新增可以 自定义 RemoteMediator 实现 network + db 的混合使用,把网络数据缓存到本地数据库,在进行加载,这样只需要删除和修改数据库的数据源就可以直接更新UI。RemoteMediator 具体使用方式可以查看谷歌的 Sample 。但如果有多个列表需要操作,每个都需要建表,而且对于何时清空数据库处理也比较麻烦。

第二种方法,可以通过 adapter.snapshot() 获取当前数据快照,对数据进行增加或删除,然后调用刷新方法,PagingSource 构建时获取修改后的数据源,就能实现。不过这种方法需要动态去判断当前获取本地数据还是网络数据,同时刷新后当前页数会重置,需要记住前一次的页数,比较麻烦。

viewModel.remove(adapter.snapshot().toMutableList().apply{ removeAt(viewHolder.layoutPosition) }.filterNotNull())
adapter.refresh()
private var loadCache = false
private var newData: List<PersonData>? = null
private var pageNum = 0

private suspend fun getData(page: Int): List<PersonData>? {
    return if (loadCache) {
        loadCache = false
        newData
    } else {
        OtherRepository.loadData(page).apply {
            //数据加载完后才能赋值当前页数
            pageNum = page
        }
    }
}

fun remove(data: List<PersonData>) {
    loadCache = true
    newData = data
}

对于删除,还有一种假删除的方法,直接把删除的item在adapter中改为高度为0的控件,数据没有变,只是把删除的项隐藏起来。

paging3可以很方便的实现列表的分页加载,但对于网络数据源列表删除,新增没有很好的办法,以上办法虽然都能实现,但都不优雅,如果有好的实现方案,欢迎分享!

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android Paging3 是一个用于在 Android 应用中实现分页加载数据的开源库。它是 Google 官方发布的最新版本的 Paging 库,相较于以往的版本,Paging3 在实现简单、功能强大和性能优化方面有了很大的改进。 首先,Android Paging3 提供了强大的数据加载和显示机制。它通过将数据分割成固定大小的数据块 (page),并在需要时按需加载和展示数据,实现了无限滚动加载的效果。相较于传统的 RecyclerView 分页加载,Paging3 更加灵活,可以自动处理数据的加载和卸载,无需手动实现判断是否到底部、加载更多等繁琐逻辑。同时,Paging3 还支持局部刷新、数据源无缝替换等操作,让数据的加载和显示更加简单和高效。 其次,Paging3 在性能方面进行了优化。它使用了异步数据加载和显示机制,可以在后台线程上进行数据加载,不会阻塞主线程。同时,Paging3 采用了数据预加载和缓存策略,可以将下一页的数据提前加载到内存中,从而提高用户体验和应用的响应速度。并且,Paging3 还支持数据的持久化存储,可以将加载的数据缓存到本地数据库或文件中,避免了重复加载数据的开销。 最后,Paging3 还提供了丰富的扩展功能和灵活的定制选项。开发者可以自定义数据加载策略、数据源类型、数据显示方式等,以满足不同的业务需求。同时,Paging3 还提供了相关的辅助类和工具方法,帮助开发者更加便捷地实现数据的分页加载和显示。 总结来说,Android Paging3 是一个功能强大、性能优越的分页加载库,可以帮助开发者轻松实现数据的分页加载和显示,提高应用的用户体验和性能表现。无论是处理大量数据的列表页,还是实现无限滚动加载的功能,Paging3 都是一个值得推荐的选择。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值