Jetpack - Paging

本文详细介绍了AndroidPaging3库的使用,包括PagingSource作为数据源的定义,Pager的配置与数据流创建,PagingDataAdapter在RecyclerView中的应用,以及LoadStateAdapter和RemoteMediator在处理加载状态和数据持久化中的角色。通过示例展示了如何在Repository和ViewModel中实现分页加载,以及如何处理加载错误和状态更新。

一、概念

相对于传统的下拉刷新上拉加载,只需要告诉Paging如何加载数据,不用再监听滑动事件操作何时加载下一页。
PagingSource用于定义数据的来源和加载方式。开发者需要实现 PagingSource 抽象类,并在其中指定如何从数据源中加载特定页的数据。PagingSource 通常用于与网络 API 或本地数据库进行交互,获取分页数据。
Pager用于创建分页数据流的类。通过 Pager,开发者可以将 PagingSource 与其他配置参数(如分页大小、预取距离等)结合起来,创建用于加载和展示分页数据的 PagingData 数据流。Pager 提供了多个静态方法,用于创建不同类型的 PagingData 数据流。Pager对象负责从PagingSource加载数据,并将数据包装成PagingData对象发射到Flow/LiveData中。
PagingData表示分页数据的类。它是一个泛型类,可以容纳各种类型的分页数据。
PagingDataAdapter是 RecyclerView.Adapter 的子类,专门用于展示 PagingData 数据流中的分页数据。PagingDataAdapter 提供了内置的数据差异计算和局部刷新机制,使得在 RecyclerView 中展示分页数据变得更加高效和简单。它还提供了加载状态和错误处理等功能。
LoadStateAdapter是一个用于展示加载状态的 RecyclerView.Adapter 的子类。它可以与 PagingDataAdapter 结合使用,用于展示分页数据的加载状态,如加载中、加载错误等。LoadStateAdapter 可以显示自定义的加载状态布局,并根据加载状态的变化自动更新 UI。
RemoteMediator是用于处理远程数据加载和数据库插入的接口。当 PagingSource 加载远程数据时,RemoteMediator 可以在加载完成后将数据插入本地数据库,并提供信息以支持分页和数据持久化。RemoteMediator 是实现离线缓存和数据持久化的关键组件之一。

1.1 数据源 PagingSource

PagingSource<Key, Value>

第一个参数 Key:表示页的标识符。在 Paging 3 中,每个页都需要一个唯一的标识符来识别它。通常情况下,这个标识符可以是整数类型,表示页的编号或索引。在加载数据时,我们可以根据这个标识符来确定要加载的是哪一页的数据。

第二个参数 Value:表示加载的数据项的类型。这个类型可以是你自定义的任何数据类型,根据你的需求而定。在分页加载过程中,每个加载的数据项都属于这个类型。

getRefreshKey()请求出错时会调用refresh方法加载 ,如果当前已经请求了第一页到第四页的数据, 可以通过设置在refresh 后会加载第5 - 8页的数据,并且前四页的数据都没了。如果getRefreshKey返回null,refresh后 会重新加载第一到第四页的数据。
load()

负责加载特定页的数据。(编写具体的数据加载逻辑、处理数据加载状态和错误、设置下一页或者前一页的key值)。

参数 LoadParams 包含了当前请求的加载信息,例如 params.key(当前请求的页数)、params.loadSize(请求的加载数量)等。
返回值 LoadResult 是一个包装类,用于封装加载结果,它可以是:

LoadResult.Page:表示成功加载了一页数据,需要提供:data(当前页的数据)、prevKey(前一页)、nextKey(后一页)。

LoadResult.Error:表示加载数据时遇到了错误,需要提供一个Throwable对象。

1.2 分页 Pager

Pager 负责将 PagingSource 与界面进行绑定,并提供可供界面使用的数据类型。

Pager

Pager (
    config: PagingConfig,        //加载配置
    initialKey: Key? = null,        //可选的初始化数据,指定初始页的键值
    remoteMediator: RemoteMediator<Key, Value>?,
    pagingSourceFactory: () -> PagingSource<Key, Value>        //指定数据源
)

调用 .flow 或 .liveData 返回需要的可观察容器。容器再调用 .cacheIn() 可将数据缓存在ViewModelScope 中。

PagingConfig

pageSize:指定每页加载多少项数据。

prefetchDistance:预取下一页数据的距离,不能为0,否则不会拉取下一页数据。

initialLoadSize:初始加载多少项数据,跟pageSize保持一致就好

二、基本使用

2.1 添加依赖

查看官方最新版本

implementation "androidx.paging:paging-runtime:3.1.1"

2.2 DataSource中定义数据源并配置分页

class MyDataSource {
    //每次拉取20条数据
    fun dataPager() = Pager(PagingConfig(20)) { MyPagingSource() }
}

/**
 * 参数一:页码的类型。参数二:item的类型(注意不是表)。
 */
private class MyPagingSource() : PagingSource<Int, Article>() {    //参数一页码,参数二API返回数据对应的实体类
    private val startPage = 0   //API的默认开始页码
    //提供对应页面的数据(分页逻辑)
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        return try {
            //拿到当前页码(为null就设置为默认开始页码)
            val currentPage = params.key ?: startPage
            //当前页获取数据逻辑(数据源在内部自己管理,而不是在Adapter中了)
            val data = getData(currentPage)
            //上一页页码(当前页是第一页,上一页就返回null)
            val prevKey = if (currentPage > startPage) currentPage - 1 else null
            //下一页页码(当前页是最后一页,下一页就返回null)
            val nextKey = if (data.isNotEmpty()) currentPage + 1 else null
            //返回结果
            LoadResult.Page(data, prevKey, nextKey)
        } catch (e: Exception) {
            //返回错误
            LoadResult.Error(e)
        }
    }

    //一般直接返回null就行
    //当分页数据因刷新、失效或重新加载时,通过计算 refreshKey 确定从哪个位置重新加载数据,避免用户界面出现跳跃或重复内容。
    override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        //anchorPosition表示用户当前屏幕可见项的近似位置,如果不存在锚点位置(例如首次加载)直接返回null触发初始加载。
        //closestItemToPosition()查找最接近锚点位置的实际数据项(Article 对象),若找不到对应项(例如数据为空),返回 null 触发默认加载逻辑。
        //尝试从 prevKey 或 nextKey 中获取最接近 anchorPosition 的页面键值;此处需进行空值处理。
        //若 prevKey == null → 则 anchorPage 为第一页
        //若 nextKey == null → 则 anchorPage 为最后一页
        //若 prevKey 与 nextKey 均为 null → 则 anchorPage 为初始页面,此时应返回 null
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    //具体获取数据的方法。这里能更细分的对异常处理,否则在load()中合并返回后在UI中难区分。
    //但处理后还是要抛出异常,不然load()不会返回异常,影响UI中对Paging状态判断
    private suspend fun getData(currentPage: Int): List<Article> {
        var data: List<Article> = emptyList()
        runCatching {
            ApiService.getData(currentPage.toString())  //获取数据
        }.onSuccess {
            data = it
        }.onFailure {
            throw it
        }
        return data
    }
}

2.3 Repository将分页转为需要的数据类型提供出去

可选,一般直接在 DataSource 中转换并提供。可转换为 Flow 或 LivaData。

class MyRepository(
    private val dataSource: MyDataSource
) {
    fun dataFlow() = dataSource.dataPager().flow
}

2.4 ViewModel 获取数据流并缓存

Pager会调用PagingSource的load( )方法获取数据,每个PagingData代表一页的数据。(冷流无法后期往里发送数据,cacheIn() 缓存后,每次收集都会返回包含过往数据的新 PagingData 实例)
class MyViewModel(
    private val repository: MyRepository
) : ViewModel() {
    //返回值Flow<PagingData<Article>>
    //将数据流在ViewModel中缓存,横竖屏切换后Paging能从缓存中读取数据而不是重新联网请求
    val dataFlow = repository.dataFlow().cachedIn(viewModelScope) 
}

2.5 PagingDataAdapter

//比较器DIFFCALLBACK通过伴生对象DiffUtil实现
//不需要传数据源进来,不需要实现条目数量,这些在PagingSource中进行
class MyPagingAdapter : PagingDataAdapter<Article, RecyclerView.ViewHolder>(DIFFCALLBACK) {
    lateinit var binding: Drawer3ItemBinding

    companion object {
        private val DIFFCALLBACK = object : DiffUtil.ItemCallback<Article>() {
            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
                return oldItem.id == newItem.id
            }
            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
                return oldItem == newItem
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val myViewHolder = holder as MyViewHolder
        val article = getItem(position) //拿到bean
        if (article != null) {
            val title = Html.fromHtml(article.title).toString()
            myViewHolder.tvTitle.text = title
            myViewHolder.tvTime.text = article.niceDate
            val author = article.author
            val shareUser = article.shareUser
            val superChapterName = article.superChapterName
            if (author.isEmpty()) {
                myViewHolder.tvUserName.text = String.format("%s · %s", superChapterName, shareUser)
            } else {
                myViewHolder.tvUserName.text = String.format("%s · %s", superChapterName, author)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MyViewHolder()
    }

    inner class MyViewHolder : RecyclerView.ViewHolder(binding.root) {
        var tvTitle = binding.tvTitle
        var tvUserName = binding.tvUsername
        var tvTime = binding.tvTime
    }
}

2.6 结合数据库缓存

class PostRemoteMediator(
    private val db: AppDatabase,
    private val api: ApiService
) : RemoteMediator<Int, Article>() {
    override suspend fun load(loadType: LoadType, state: PagingState<Int, Article>): MediatorResult {
        return try {
            val page = when (loadType) {
                LoadType.REFRESH -> 1
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
                LoadType.APPEND -> db.postDao().getNextPageKey() ?: return MediatorResult.Success(true)
            }
            val posts = api.getPosts(page, state.config.pageSize)
            db.runInTransaction {
                if (loadType == LoadType.REFRESH) db.postDao().clearAll()
                db.postDao().insertAll(posts)
                db.postDao().updateNextPageKey(page + 1)
            }
            MediatorResult.Success(endOfPaginationReached = posts.isEmpty())
        } catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }
}

2.7 UI

    private fun initView() {
        val progressBar = binding.progressBar
        val recyclerView = binding.recyclerView
        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = MyPagingAdapter()
        recyclerView.adapter = adapter
        //设置加载状态监听(也可以写成adapter.loadStateFlow.collectLatest{}对it进行分类)
        adapter.addLoadStateListener {
            //it.refresh:在初始化刷新的使用(也就是说第二页第三页...是监听不到的)
            //it.append:在加载更多的时候使用
            //it.prepend:在当前列表头部添加数据的时候使用
            when (it.refresh) {
                //当没有加载动作并且没有错误的时候
                is LoadState.NotLoading -> {
                    progressBar.visibility = View.INVISIBLE
                    recyclerView.visibility = View.VISIBLE
                }
                //正在加载
                is LoadState.Loading -> {
                    progressBar.visibility = View.VISIBLE
                    recyclerView.visibility = View.INVISIBLE
                }
                //加载错误(这里的错误是PagingSource里捕获的)
                is LoadState.Error -> {
                    val state = it.refresh as LoadState.Error
                    progressBar.visibility = View.INVISIBLE
                    showToast("adapter报错: ${state.error.message}")
                    adapter.retry()    //调用重试
                }
            }
        }
    }

    private fun byFlow() {
        lifecycleScope.launch {
            viewModel.getData().collect { pagingData ->
                adapter.submitData(pagingData)  //提交数据后Paging就开始工作了
            }
        }
    }

//    private fun byLiveData() {
//        viewModel.dataLiveData.observe(this) { pagingData ->
//            lifecycleScope.launch {
//                adapter.submitData(pagingData)
//            }
//        }
//    }

三、添加Footer、Header

//通过构造传入重试的方法,在UI中直接传入PagingAdapter.retry()
class Drawer3FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<RecyclerView.ViewHolder>() {
    private lateinit var binding: Drawer3FooterBinding

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, loadState: LoadState) {
        val footViewHolder = holder as FootViewHolder
        //根据LoadState状态来控制脚部界面显示(加载/重试)
        footViewHolder.progressBar.isVisible = loadState is LoadState.Loading
        footViewHolder.retryButton.isVisible = loadState is LoadState.Error
        footViewHolder.retryButton.setOnClickListener {
            retry
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): RecyclerView.ViewHolder {
        binding = Drawer3FooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return FootViewHolder()
    }

    inner class FootViewHolder : RecyclerView.ViewHolder(binding.root) {
        val progressBar = binding.progressBar
        val retryButton = binding.retryButton
    }
}
adapter = MyPagingAdapter()
val concatAdapter = adapter.withLoadStateFooter(Drawer3FooterAdapter { adapter.retry() })  //添加脚部
recyclerView.adapter = concatAdapter

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值