Android Paging3 基本使用

本文介绍了Android Paging3的基本使用,包括添加依赖、配置数据源(如Room与自定义数据源)、构建分页数据、创建RecyclerView.Adapter、展示分页列表、监听数据加载状态以及设置Header和Footer。Paging3简化了分页加载的复杂性,提供了Room集成和自定义PagingSource的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

Paging3,是Jetpack提供给开发者用来显示本地或者网络数据集的分页库。针对这类场景,传统的做法是用RecyclerView的加载更多来实现分页加载,很多逻辑需要自行处理且不一定完善。Paging3相当于是官网提供的一套解决方案。

下图为您应用的各个层级中推荐直接接入 Paging 的 Android 应用架构
在这里插入图片描述

二、添加依赖

根据语言二选一即可,我使用的是kotlin

//java
implementation 'androidx.paging:paging-runtime:3.0.0-alpha09'
//kotlin
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha09'

三、基本使用

基本使用主要包含如下内容:

  • 配置数据源:PagingSource
  • 构建分页数据:Pager、PagingData
  • 构建RecyclerView Adapter:PagingDataAdapter
  • 展示分页UI列表数据
  • 设置Header和Footer
  • 监听数据加载的状态

3.1 配置数据源

abstract class PagingSource<Key : Any, Value : Any> 

参数解析:

  • Key:分页标识类型,如页码,则为Int
  • Value:返回列表元素的类型

3.1.1 Room

如果使用的是Room,从 2.3.0-alpha 开始,它将默认为您实现 PagingSource。在定义 Dao 接口的 Query 语句时,返回类型要使用 PagingSource 类型。同时不需要在 Query 里指定页数和每页展示数量,页数由 PagingSource 来控制,每页数量页在 PagingConfig 中定义。

@Dao
interface UserDao {
    @Query("SELECT * FROM User ORDER BY name COLLATE NOCASE ASC")
    fun allUserByName(): PagingSource<Int, User>

    @Insert
    fun insert(user: User)

    @Delete
    fun delete(user: User)
}

使用 Room 有一个好处是,如果通过 insert() 或 delete() 等方法修改了 Room 里的数据,不需要额外处理就会即时反应在 PagingSource 里,界面上展示的数据会相应变化。

3.1.2 自定义数据源

使用PagingSource
如果不是直接使用 Room 的数据,而是使用源自其他地方的数据,比如网络数据,就需要自定义 PagingSource 了,创建方式如下:

class UserDataSource : PagingSource<Int, User>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        return try {
            val page = params.key ?: 0
            //获取网络数据
            val result = Retrofitance.instance.ApiService.getUsers(page)
            LoadResult.Page(
                //需要加载的数据
                data = result.data,
                //如果可以往上加载更多就设置该参数,否则不设置
                prevKey = null,
                //加载下一页的key 如果传null就说明到底了
                nextKey = if (result.curPage == result.pageCount) null else page + 1
            )
        } catch (e: IOException) {
            // IOException for network failures.
            return LoadResult.Error(e)
        } catch (e: HttpException) {
            // HttpException for any non-2xx HTTP status codes.
            return LoadResult.Error(e)
        }
    }
}

参数解释:

  • data :返回的数据列表
  • prevKey :上一页的key (传 null 表示没有上一页)
  • nextKey :下一页的key (传 null 表示没有下一页)
  • paging3 使用 flow 传递数据,不了解的可以搜索一下flow ;
  • cachedIn:绑定协程生命周期,必须加上,否则可能崩溃;
  • asLiveData:熟悉livedata的都知道怎么用;

使用RemoteMediator

RemoteMediator 和 PagingSource 相似,都需要覆盖 load() 方法,但是不同的是 RemoteMediator 不是加载分页数据到 RecyclerView 列表上,而是获取网络分页数据并更新到数据库中。

区别:

  • PagingSource:实现单一数据源以及如何从该数据源中查找数据,例如 Room,数据源的变动会直接映射到 UI 上
  • RemoteMediator:实现加载网络分页数据并更新到数据库中,但是数据源的变动不能直接映射到 UI 上

在项目中如何进行选择?

  • PagingSource:用于加载有限的数据集(本地数据库)例如手机通讯录等等。
  • RemoteMediator:主要用来加载网络分页数据并更新到数据库中,当我们没有更多的数据时,我们向网络请求更多的数据,结合 PagingSource 当保存更多数据时会直接映射到 UI 上

注意:
RemoteMediator 目前是实验性的 API ,所有实现 RemoteMediator 的类都需要添加 @OptIn(ExperimentalPagingApi::class) 注解。

当我们使用 OptIn 注解,需要在 App 模块下的 build.gradle 文件内添加以下代码

android {
    kotlinOptions {
        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
    }
}

@OptIn(ExperimentalPagingApi::class)
class UserRemoteMediator(
    val api: ApiService,
    val db: AppDataBase
) : RemoteMediator<Int, User>() {

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, User>
    ): MediatorResult {
        try {

            /**
             * 在这个方法内将会做三件事
             *
             * 1. 参数 LoadType 有个三个值,关于这三个值如何进行判断
             *      LoadType.REFRESH
             *      LoadType.PREPEND
             *      LoadType.APPEND
             *
             * 2. 访问网络数据
             *
             * 3. 将网路插入到本地数据库中
             */

            var pageCount = Int.MAX_VALUE
            var curPage = 0

            // 第一步: 判断 LoadType
            val pageKey = when (loadType) {
                // 首次访问 或者调用 PagingDataAdapter.refresh()
                LoadType.REFRESH -> null

                // 在当前加载的数据集的开头加载数据时
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)

				// 上拉加载更多时触发
                LoadType.APPEND -> return if (curPage == result) MediatorResult.Success(endOfPaginationReached = false) else curPage + 1
            }

            // 第二步: 请问网络分页数据
            val page = pageKey ?: 0
            //获取网络数据
            val result = Retrofitance.instance.ApiService.getUsers(page)
            curPage = result.curPage
            pageCount = result.pageCount
            val data = result.data
            val endOfPaginationReached = data.isEmpty()
            
            // 第三步: 插入数据库
            val userDao = db.userDao()
            if (!endOfPaginationReached){
            	userDao.insertUsers(data)
            }
            return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
        } catch (e: IOException) {
            return MediatorResult.Error(e)
        } catch (e: HttpException) {
            return MediatorResult.Error(e)
        }
    }
}

LoadType 是一个枚举类,里面定义了三个值,如下所示

类名作用
LoadType.Refresh在初始化刷新的使用,首次访问 或者调用 PagingDataAdapter.refresh() 触发
LoadType.Append在加载更多的时候使用,需要注意的是当 LoadType.REFRESH 触发了,LoadType.PREPEND 也会触发
LoadType.Prepend在当前列表头部添加数据的时候使用

PagingState:这个类当中有两个重要的变量

  • pages: List<Page<Key, Value>> 返回的上一页的数据,主要用来获取上一页最后一条数据作为下一页的开始位置
  • config: PagingConfig 返回的初始化设置的 PagingConfig 包含了 pageSize、prefetchDistance、initialLoadSize 等等

load() 的返回值 MediatorResult,MediatorResult 是一个密封类,根据不同的结果返回不同的值

  • 请求出现错误,返回 MediatorResult.Error(e)
  • 请求成功且有数据,返回 MediatorResult.Success(endOfPaginationReached = true)
  • 请求成功但是没有数据,返回 MediatorResult.Success(endOfPaginationReached = false)

3.2 构建分页数据

分页数据的容器被称为 PagingData,每次刷新数据时,都会创建一个 PagingData的实例。如果要创建 PagingData 数据流,您需要创建一个 Pager 实例,并提供一个 PagingConfig 配置对象和一个可以告诉 Pager 如何获取您实现的 PagerSource 的实例的函数,以供 Pager 使用。

class PagingViewModel : ViewModel() {

    val allUser: Flow<PagingData<User>> = Pager(
            PagingConfig(
                    // 每页显示的数据的大小。对应 PagingSource 里 LoadParams.loadSize
                    pageSize = 20,

                    // 预刷新的距离,距离最后一个 item 多远时加载数据,默认为 pageSize
                    prefetchDistance = 3,

                    // 初始化加载数量,默认为 pageSize * 3
                    initialLoadSize = 60,

                    // 一次应在内存中保存的最大数据,默认为 Int.MAX_VALUE
                    maxSize = 200
            ),
            remoteMediator = UserRemoteMediator(api, db)
    ) {
        // 数据源,要求返回的是 PagingSource 类型对象
        UserDataSource()
    }.flow.cachedIn(viewModelScope) // 最后构造的和外部交互对象,有 flow 和 liveData 两种

}    

3.3 构建RecyclerView Adapter

为了将 RecyclerView 与 PagingData 联系起来,您需要实现一个 PagingDataAdapter。adpater 构造必须传参数 DiffUtil.ItemCallback,对DiffUtil.ItemCallbackAsyncListDiffer不了解的可以参考一下这篇文章:Android RecyclerView的正确打开方式——DiffUtil、AsyncListDiff以及最新ListAdapter使用

PagingDataAdapter的使用方式与ListAdapter差不多

class UserAdapter : PagingDataAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val itemView: View = LayoutInflater.from(parent.context).inflate(R.layout.item_user_list, parent, false)
        return ViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private var tvName: TextView? = null
        private var tvAge: TextView? = null

        init {
            tvName = itemView.findViewById(R.id.tv_name)
            tvAge = itemView.findViewById(R.id.tv_age)
        }

        fun bind(user: User) {
            tvName?.text = user.name
            tvAge?.text = user.age.toString()
        }
    }
}

private class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.age == newItem.age
    }
}

3.4 展示分页列表数据

class PagingActivity : AppCompatActivity() {

    private val viewModel by lazy { ViewModelProvider(this).get(PagingViewModel::class.java) }

    private val adapter by lazy { UserAdapter() }

    private val binding by lazy {
        ActivityPagingBinding.inflate(layoutInflater)
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        
        binding.recyclerView.adapter = adapter 
        
        lifecycleScope.launch {
            viewModel.allUser.collectLatest { pagingData ->
                adapter.submitData(pagingData)
            }
        }
    }
}

adapter.submitData 是一个协程挂起(suspend)操作,所以要放入协程赋值

3.5 监听数据加载的状态

除了上面的 override fun onBindViewHolder(holder: BindingViewHolder, loadState: LoadState)会返回数据的加载状态,我们还可以使用监听器

想要监听数据获取的状态在PagingDataAdapter里有两个方法

  • addDataRefreshListener 这个方法是当新的PagingData被提交并且显示的回调
  • addLoadStateListener这个相较于上面那个比较复杂,listener中的回调会返回一个CombinedLoadStates对象
data class CombinedLoadStates(
    /**
     * [LoadStates] corresponding to loads from a [PagingSource].
     */
    val source: LoadStates,
 
    /**
     * [LoadStates] corresponding to loads from a [RemoteMediator], or `null` if RemoteMediator
     * not present.
     */
    val mediator: LoadStates? = null
) {
    val refresh: LoadState = (mediator ?: source).refresh
    val prepend: LoadState = (mediator ?: source).prepend
    val append: LoadState = (mediator ?: source).append
...
}
  • refresh:LoadState:刷新时的状态,因为可以调用 PagingDataAdapter#refresh() 方法进行数据刷新。
  • append:LoadState:可以理解为 RecyclerView 向下滑时数据的请求状态。
  • prepend:LoadState:可以理解为RecyclerView 向上滑时数据的请求状态。
  • sourcemediator 分别包含上面3个的属性,source 代表单一的数据源,mediator 代表多数据源的场景,source 和 mediator 二选一。
adapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.Loading -> {}
                is LoadState.NotLoading -> {}
                is LoadState.Error -> {}
            }
        }

状态对应的类是 LoadState,它有三种状态:

  • Loading:数据加载中。
  • NotLoading:内存中有已经获取的数据,即使往下滑,Paging 也不需要请求更多的数据。
  • Error:请求数据时返回了一个错误。

一般来说,对于可下拉刷新或者上拉加载更多的视图,我们都会用第三方框架,这样会更好处理,并且有默认写好的上拉或下拉的样式。以SmartRefreshLayout为例:

    private fun onSubscribeUi(binding: ActivityPagingBinding) {

        var currentStates:LoadStates? = null
        // 数据加载状态的回调
        adapter.addLoadStateListener { state:CombinedLoadStates ->
            currentStates = state.source
            // 如果append没有处于加载状态,但是refreshLayout出于加载状态,refreshLayout停止加载状态
            if (state.append is LoadState.NotLoading && binding.refreshLayout.isLoading) {
                binding.refreshLayout.finishLoadMore()
            }
            // 如果refresh没有出于加载状态,但是refreshLayout出于刷新状态,refreshLayout停止刷新
            if (state.source.refresh is LoadState.NotLoading && binding.refreshLayout.isRefreshing) {
                binding.refreshLayout.finishRefresh()
            }
        }

        binding.refreshLayout.setRefreshHeader(DropBoxHeader(context))
        binding.refreshLayout.setRefreshFooter(ClassicsFooter(context))
        binding.refreshLayout.setOnLoadMoreListener {
            // 如果当前数据已经全部加载完,就不再加载
            if(currentStates?.append?.endOfPaginationReached == true)
                binding.refreshLayout.finishLoadMoreWithNoMoreData()
        }

        //... 省略无关代码
    }

刷新失败处理
直接调用刷新即可

adapter.refresh()

加载更多失败处理

adapter.retry()

为什么是重试?
因为paging是无缝加载,所以没有手动上拉加载逻辑
retry()虽然是重试,但是paging已处理,只有失败后会重试,所以这里上拉加载调用重试没问题

3.6 设置Header和Footer

除了使用第三方框架,Paging 3.0支持添加 Header 和 Footer,官方示例是把它们用作上拉刷新和下拉加载更多的控件。

3.6.1 Create the view layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:padding="8dp">
    <TextView
        android:id="@+id/error_msg"
        android:textColor="?android:textColorPrimary"
        android:textSize="@dimen/error_text_size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textAlignment="center"
        tools:text="Timeout"/>
    <ProgressBar
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>
</LinearLayout>
3.6.2 Create the LoadStateAdapter
class UserLoadStateAdapter(private val adapter: UserAdapter) : LoadStateAdapter<UserLoadStateAdapter.BindingViewHolder>() {

    override fun onCreateViewHolder(
            parent: ViewGroup,
            loadState: LoadState
    ): BindingViewHolder {
        val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return BindingViewHolder(binding, adapter)
    }

    override fun onBindViewHolder(holder: BindingViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }

    class BindingViewHolder(
            private val binding: ItemFooterBinding,
            private val adapter: UserAdapter
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(loadState: LoadState) {

            when (loadState) {
                is LoadState.Error -> {
                    binding.progressBar.visibility = View.GONE
                    binding.errorMsg.visibility = View.VISIBLE
                    binding.errorMsg.text = "Load Failed, Tap Retry"
                    binding.errorMsg.setOnClickListener {
                        adapter.retry()
                    }
                }
                is LoadState.Loading -> {
                    binding.progressBar.visibility = View.VISIBLE
                    binding.errorMsg.visibility = View.VISIBLE
                    binding.errorMsg.text = "Loading"
                }
                is LoadState.NotLoading -> {
                    binding.progressBar.visibility = View.GONE
                    binding.errorMsg.visibility = View.GONE
                }
            }
        }

    }
}
binding.recyclerView.adapter = adapter.withLoadStateHeaderAndFooter(
    UserLoadStateAdapter(adapter),
    UserLoadStateAdapter(adapter)
)
方法使用
fun withLoadStateHeader(header: LoadStateAdapter<*> )头部添加状态适配器
fun withLoadStateFooter(footer: LoadStateAdapter<*> )底部添加状态适配器
fun withLoadStateHeaderAndFooter(header: LoadStateAdapter<>, footer: LoadStateAdapter<> )头尾都添加状态适配器

四、总结

  • PagingSource:负责提供源数据,一般是网络请求或者数据库查询,
  • PagingData:分页数据的容器,负责一些分页的参数设定和订阅源数据流
  • PagingDataAdapter:跟常规的RecyclerivewAdapter一样把数据转换成UI
  • LoadStateAdapter:可以添加页头/页脚,方便实现loadmore的样式
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值