Jetpack - Paging

一、概念

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

1.1 Repository 层定义数据源 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 ViewModel 层定义 Pager

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

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

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

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

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

二、基本使用

2.1 添加依赖

查看官方最新版本

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

2.2 定义数据源 PagingSource

/**
 * 参数一:页码的类型。参数二:item的类型(注意不是表)。
 * PagingSource会在ViewModel中调用,ViewModel一般写法会创建Repository实例,
 * 由于Repository中对于数据来源进行了封装,因此构造传入Repository实例来调用获取数据的方法会更好。
 */
class MyPagingSource(
    private val repository: MyRepository
) : PagingSource<Int, HotNewestArticleBean.HotNewestArticle.Article>() {    //参数一页码,参数二API返回数据对应的实体类
    private val startPage = 0   //API的默认开始页码
    //提供对应页面的数据(分页逻辑)
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, HotNewestArticleBean.HotNewestArticle.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从第一页加载。
    //例如当前已经请求了1-4页,可以通过设置在刷新后会加载5-8页数据,而1-4页数据都没了。
    override fun getRefreshKey(state: PagingState<Int, HotNewestArticleBean.HotNewestArticle.Article>): Int? {
        return null
    }

    //具体获取数据的方法。这里能更细分的对异常处理,否则在load()中合并返回后在UI中难区分。
    //但处理后还是要抛出异常,不然load()不会返回异常,影响UI中对Paging状态判断
    private suspend fun getData(currentPage: Int): List<HotNewestArticleBean.HotNewestArticle.Article> {
        var data: List<HotNewestArticleBean.HotNewestArticle.Article> = emptyList()
        runCatching {
            repository.getData(currentPage.toString())  //通过仓库获取数据,
        }.onSuccess { response ->
            response.getData().onSuccess {
                data = it.datas
            }.onFailure {
                Log.e("服务器错误", it.message.toString())
                throw Exception("服务器错误:${it.message}")
            }
        }.onFailure {
            Log.e("本地错误", it.message.toString())
            throw Exception("本地错误:${it.message}")
        }
        return data
    }
}

2.3 ViewModel层定义Pager

Pager会调用PagingSource的load( )方法获取数据,每个PagingData代表一页的数据。
class MyViewModel : ViewModel() {
    private val repository = Drawer3Repository()
    val dataLiveData by lazy {
        Pager(PagingConfig(pageSize = 15)) {
            MyPagingSource(repository)
        }.liveData.cachedIn(viewModelScope)
    }
    //返回值Flow<PagingData<HotNewestArticleBean.HotNewestArticle.Article>>
    //PagingData<>里面包含的是item类型,这里用的是Flow,也可以用上面的LiveData
    //通过PagingConfig配置一页加载的item数量(pageSize)
    fun getData() = Pager(PagingConfig(pageSize = 15)) {
            MyPagingSource(repository)
        }.flow.cachedIn(viewModelScope) //将数据在ViewModel中缓存,横竖屏切换后Paging能从缓存中读取数据而不是重新联网请求。
}

2.4 PagingDataAdapter

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

    companion object {
        private val DIFFCALLBACK = object : DiffUtil.ItemCallback<HotNewestArticleBean.HotNewestArticle.Article>() {
            override fun areItemsTheSame(oldItem: HotNewestArticleBean.HotNewestArticle.Article, newItem: HotNewestArticleBean.HotNewestArticle.Article): Boolean {
                return oldItem.id == newItem.id
            }
            override fun areContentsTheSame(oldItem: HotNewestArticleBean.HotNewestArticle.Article, newItem: HotNewestArticleBean.HotNewestArticle.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.5 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}")
                }
            }
        }
    }

    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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值