Android Paging3分页+ConcatAdapter+空数据视图+下拉刷新(SwipeRefreshLayout)+加载更多+错误重试 (示例)

news2024/11/23 5:14:34

文章目录

  • 引入库
  • 数据模型定义
  • 分页 adapter
  • 加载更多 adapter
  • 空数据 adapter
  • 分页数据源
  • ViewModel 提供加载数据源的方法
  • 结合以上实现的 Fragment
  • 数据重复问题

请添加图片描述

引入库

implementation 'androidx.paging:paging-runtime-ktx:3.1.1'

paging 库,目前还是有点小bug ,后面说


数据模型定义

// 分页请求的数据响应,至少要返回一个总数量; page、pageIndex 都可本地定义,然后 可以计算出当前请求响应后,历史返回的总数量;最终计算出是否还有下一页
data class CustomerPageData(val totalCount: Int, val data: List<CustomerData>)

data class CustomerData(val id: String, val name: String)

分页 adapter

import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.common.BaseViewHolder
import com.stone.stoneviewskt.data.CustomerData
import com.stone.stoneviewskt.util.logi

/**
 * desc:
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 11:20
 */
class PageListAdapter: PagingDataAdapter<CustomerData, BaseViewHolder>(object : DiffUtil.ItemCallback<CustomerData>() {
    /*
     * 如果 areItemsTheSame 返回 true,但是仍然会展示相同的数据,可能是因为 areContentsTheSame 返回 false
     */

    // 是否是同一条 item
    override fun areItemsTheSame(oldItem: CustomerData, newItem: CustomerData): Boolean {
        logi("areItemsTheSame") // 没有输出该日志  可能是官方的bug(不然实现这个回调有什么意义)
        return oldItem.id == newItem.id
    }

    // 是否内容相同
    override fun areContentsTheSame(oldItem: CustomerData, newItem: CustomerData): Boolean {
        logi("areContentsTheSame") // 没有输出该日志 可能是官方的bug(不然实现这个回调有什么意义)
        return oldItem.id == newItem.id
    }
}) {

    // item 点击事件
    private var mBlock: ((position: Int, data: CustomerData) -> Unit)? = null

    override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
        val data = getItem(position) ?: return
        holder.findView<TextView>(R.id.tv_name).text = data.name

        holder.itemView.setOnClickListener {
            mBlock?.invoke(position, data)
        }

        if (position % 2 == 0) {
            holder.itemView.setBackgroundColor(Color.parseColor("#c0ff00ff"))
        } else {
            holder.itemView.setBackgroundColor(Color.parseColor("#c0abc777"))
        }
    }

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

    fun setItemClick(block: (position: Int, data: CustomerData) -> Unit) {
        this.mBlock = block
    }

}

加载更多 adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.common.BaseViewHolder
import com.stone.stoneviewskt.util.logi

/**
 * desc:    加载更多
 * author:  stone
 * email:   aa86799@163.com
 * time:    2023/6/8 16:11
 */
class LoadMoreAdapter(private val retryBlock: () -> Unit) : LoadStateAdapter<BaseViewHolder>() {
    override fun onBindViewHolder(holder: BaseViewHolder, loadState: LoadState) {
        when (loadState) {
            is LoadState.NotLoading -> { // 非加载中
                logi("LoadMoreAdapter---onBindViewHolder---LoadState.NotLoading")
                holder.itemView.findViewById<ProgressBar>(R.id.progressBar).visibility = View.GONE
                holder.itemView.findViewById<TextView>(R.id.tv_load).visibility = View.GONE

                holder.itemView.findViewById<TextView>(R.id.tv_all_message).visibility = View.VISIBLE

                holder.itemView.findViewById<TextView>(R.id.btn_retry).visibility = View.GONE
            }

            is LoadState.Loading -> { // 加载中
                holder.itemView.findViewById<ProgressBar>(R.id.progressBar).visibility = View.VISIBLE
                holder.itemView.findViewById<TextView>(R.id.tv_load).visibility = View.VISIBLE

                holder.itemView.findViewById<TextView>(R.id.tv_all_message).visibility = View.GONE

                holder.itemView.findViewById<TextView>(R.id.btn_retry).visibility = View.GONE
            }

            is LoadState.Error -> {
                holder.itemView.findViewById<ProgressBar>(R.id.progressBar).visibility = View.GONE
                holder.itemView.findViewById<TextView>(R.id.tv_load).visibility = View.GONE

                holder.itemView.findViewById<TextView>(R.id.tv_all_message).visibility = View.GONE

                val btnRetry = holder.itemView.findViewById<TextView>(R.id.btn_retry)
                btnRetry.visibility = View.VISIBLE
                btnRetry.text = "发生了错误:${loadState.error.message},点击重试"
                btnRetry.setOnClickListener {
                    retryBlock()
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): BaseViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.layout_load_more, parent, false)
        return BaseViewHolder(itemView)
    }

    // 是否作为 item 显示
    override fun displayLoadStateAsItem(loadState: LoadState): Boolean {
        logi("loadState -- $loadState")
        return loadState is LoadState.Loading
                || loadState is LoadState.Error
                || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached)
        // loadState.endOfPaginationReached false 表示有更多的数据要加载
    }

}

空数据 adapter

import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.adapter.BaseRvAdapter
import com.stone.stoneviewskt.common.BaseViewHolder

/**
 * desc:    空数据
 * author:  stone
 * email:   aa86799@163.com
 * time:    2023/6/9 10:45
 */
class EmptyAdapter: BaseRvAdapter<String>(R.layout.layout_empty) {

    override fun fillData(holder: BaseViewHolder, position: Int, data: String) {

    }
}

分页数据源

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.stone.stoneviewskt.BuildConfig
import com.stone.stoneviewskt.data.CustomerData
import com.stone.stoneviewskt.data.CustomerPageData
import com.stone.stoneviewskt.util.logi
import com.stone.stoneviewskt.util.showShort
import kotlinx.coroutines.delay
import kotlin.random.Random

/**
 * desc:    分页数据源
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 11:26
 */
class PageListPageSource : PagingSource<Int, CustomerData>() {

    private var flagA = false
    private var flagB = false
    companion object {
        private var mockEmpty = true // 静态字段 模拟空数据
    }

    override fun getRefreshKey(state: PagingState<Int, CustomerData>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomerData> {
        if (BuildConfig.DEBUG) { // 防止加载数据过快,loading 效果看不到
            delay(1000)
        }
        val currentPage = params.key ?: 1 // 当前页码
        val pageSize = params.loadSize // 每页多少条数据
        var bean: CustomerPageData? = null // 分页数据
        kotlin.runCatching { // 异常发生后,执行 onFailure {}
            // todo 真实开发时,移除模拟错误代码

            // 模拟 第2页 一定抛异常
            if (currentPage == 2 && !flagB) {
                flagA = true
            }
            if (flagA) {
                flagA = false
                flagB = true
                throw IllegalStateException("test-error")
            }


            if (currentPage % 2 == 0 && Random.nextInt() % 2 == 0) { // 当偶数页时,随机发生异常。  注意异常发生后,对请求的页码 -- currentPage是否有影响
                throw IllegalStateException("test-error")
            }
//            Api.createRetrofit(CustomerApiService::class.java).customerList(currentPage,pageSize) // 真实网络请求方式
            mockData(currentPage, pageSize)// todo 真实开发时,移除模拟数据代码
        }.onSuccess {
            bean = it
        }.onFailure {
            it.printStackTrace()
            if (it !is java.util.concurrent.CancellationException) {
                showShort("出错了" + it.message)
                return LoadResult.Error(it) // 如果返回 error,那后续 加载更多将不会触发
            }
        }
        val prevKey = if (currentPage > 1) currentPage - 1 else null // 当前页不是第一页的话,前一页为当前页减一
        val hasMore = currentPage * pageSize < (bean?.totalCount ?: 0)
        val nextKey = if (hasMore) currentPage + 1 else null // 当前页有数据的话,下一页就是,当前页加一,反之为空
        logi("currentPage:$currentPage    pageSize:$pageSize    prevKey:$prevKey     nextKey:$nextKey")
        return try {
            LoadResult.Page(
                data = bean?.data ?: arrayListOf(), // 数据
                prevKey = prevKey, // 为当前页的前一页
                nextKey = nextKey  // 为当前页的下一页
            )
        } catch (e: Exception) {
            e.printStackTrace()
            return LoadResult.Error(e)
        }
    }

    // 模拟分页数据
    private fun mockData(pageIndex: Int, pageSize: Int): CustomerPageData? {
        if (mockEmpty) {
            mockEmpty = false
            return null
        }
        logi("分页参数:[pageIndex-$pageIndex] [pageSize-$pageSize]")
        val list = arrayListOf<CustomerData>()
        val totalCount = 55
        val currentCount = if (totalCount > pageIndex * pageSize) pageSize else totalCount - (pageIndex - 1) * pageSize
         (1..currentCount).forEach {
             val currentPosition = (pageIndex - 1) * pageSize + it
             list.add(CustomerData("id-$currentPosition", "name-$currentPosition"))
        }
        return CustomerPageData(totalCount, list)
    }
}

ViewModel 提供加载数据源的方法

/**
 * desc:
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 12:12
 */
class PageListViewModel : ViewModel() {

    // todo 截止到 3.1.1版本,官方库有bug:超出 initialLoadSize 所在页码后, 再retry 没问题;反之retry,会数据重复
    // todo 为了规避 建议 真实开发时,initialLoadSize 和 pageSize 的值相同。 或看看后续版本是否有修复

    fun loadData(): Flow<PagingData<CustomerData>> {
        return Pager( //PagingConfig 定义一次从PagingSource加载的项数。
            config = PagingConfig(
                pageSize = 10,
                initialLoadSize = 15, // 初始取的 pageSize,通常要大于指定的 pageSize   但大于了,在retry时又会出现bug
                prefetchDistance = 1   // prefetchDistance条数据 时加载下一页
            ),
            //每页都会调用一次,请求数据
            pagingSourceFactory = { PageListPageSource() }
        ).flow.distinctUntilChangedBy { it }.cachedIn(viewModelScope) // 设置缓存
    }
}

结合以上实现的 Fragment

import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.ConcatAdapter
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.common.BaseBindFragment
import com.stone.stoneviewskt.databinding.FragmentPageListBinding
import com.stone.stoneviewskt.util.logi
import com.stone.stoneviewskt.util.showShort
import kotlinx.coroutines.flow.collectLatest

/**
 * desc:    Paging3分页+ConcatAdapter+空数据视图+下拉刷新(SwipeRefreshLayout)+加载更多+错误重试
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 11:09
 */
class PageListFragment: BaseBindFragment<FragmentPageListBinding>(R.layout.fragment_page_list) {

    private val viewModel by lazy { ViewModelProvider(this)[PageListViewModel::class.java] }
    private lateinit var concatAdapter: ConcatAdapter // 可合并 adapter
    private val adapter by lazy { PageListAdapter() }
    private val loadMoreAdapter by lazy {
        LoadMoreAdapter { // 点击 重试按钮
            adapter.retry()
        }
    }
    private val emptyAdapter: EmptyAdapter by lazy { EmptyAdapter() }

    override fun onPreparedView(savedInstanceState: Bundle?) {
        super.onPreparedView(savedInstanceState)

        concatAdapter = adapter.withLoadStateFooter(loadMoreAdapter) // 将 loadMoreAdapter 添加为 footer 效果
        emptyAdapter.updateData(listOf("")) // 向 emptyAdapter 添加一项,当其显示时,只显示一个对应视图
        mBind.rvPage.adapter = concatAdapter // 这时 已 添加了 loadMoreAdapter

        // 设置加载状态监听
        adapter.addLoadStateListener {
            logi("监听到loadState -- $it")
            // 如果 状态是 未加载(加载完成也算) 或 发生错误
            if (it.refresh is LoadState.NotLoading) {
                mBind.swipeRefresh.isRefreshing = false
                if (adapter.itemCount == 0) { // 真实数据为空
                    if (concatAdapter.adapters.contains(loadMoreAdapter)) {
                        concatAdapter.removeAdapter(loadMoreAdapter) // 移除加载更多
                        logi("removeAdapter -- loadMoreAdapter")
                    }
                    if (!concatAdapter.adapters.contains(emptyAdapter)) {
                        concatAdapter.addAdapter(emptyAdapter) // 添加空数据视图   仅在未添加过时才添加
                    }
                } else {
                    if (concatAdapter.adapters.contains(emptyAdapter)) {
                        concatAdapter.removeAdapter(emptyAdapter)
                        concatAdapter.addAdapter(loadMoreAdapter) // 只添加一次 loadMore
                    }
                }

            } else if (it.refresh is LoadState.Error) {
                mBind.swipeRefresh.isRefreshing = false
            }
        }

        adapter.setItemClick { position, data ->
            showShort("点击查看 --- ${data.name}")
        }

        mBind.swipeRefresh.setOnRefreshListener {
            loadData()
        }

        mBind.swipeRefresh.isRefreshing = true
        loadData()
    }

    private fun loadData() {
        lifecycleScope.launchWhenResumed {
            viewModel.loadData().collectLatest { // 加载数据
                adapter.submitData(it) // 绑定分页数据源
            }
        }
    }
}

数据重复问题

截止到 3.1.1版本,官方库有bug:超出 initialLoadSize 所在页码后, 再retry 没问题;反之retry,会数据重复。比如 pageSize=10,initialLoadSize=15,初始加载15个数据后,当前页码对应 pageSize=10 的情形下是 2;这时再请求第2页,发生了异常,调用 adapter#retry()后,会把第二页的10个条目加载进来并显示,这就发生了数据重复。而DiffUtil.ItemCallback中的两个函数,一直都没有被回调,没有任何日志打印出来… 这就很迷幻了…

为了规避该问题,建议 真实开发时,initialLoadSize 和 pageSize 的值相同。 或看看后续版本是否有修复


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/631034.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java开发技巧-数据结构-使用HashSet判断主键是否存在、使用Pair成对结果返回/Triple三个对象返回

场景 Java中使用HashSet判断主键是否存在 HashSet实现Set接口&#xff0c;由哈希表&#xff08;实际上是HashMap&#xff09;实现&#xff0c;但不保证set的迭代顺序&#xff0c;并允许使用null元素。 HashSet的时间复杂度跟HashMap一致&#xff0c;如果没有哈希冲突则时间复…

EXCEL函数笔记1(数学函数、文本函数、日期函数)

数学函数 取整&#xff1a;INT(number) 取余&#xff1a;MOD(number,除数) 四舍五入&#xff1a;ROUND(number&#xff0c;保留几位小数) 取绝对值&#xff1a;ABS(number) 根号处理&#xff1a;SQRT&#xff08;number&#xff09; 0到1随机数&#xff1a;RAND&#xff08;&am…

Python神器Anaconda图文安装教程

来源&#xff1a;投稿 作者&#xff1a;Fairy 编辑&#xff1a;学姐 Anaconda简介 Anaconda是一种数据科学和机器学习的开发环境&#xff0c;它包含了大量的Python包、工具和库&#xff0c;以及可视化界面和集成开发环境。「Anaconda可以方便地管理Python环境和安装第三方软件…

⑧电子产品拆解分析-1拖4USB拓展坞

⑧电子产品拆解分析-1拖4USB拓展坞 一、功能介绍二、电路分析以及器件作用1、内部电路拆解 三、参考资料学习 一、功能介绍 ①USB2.0一拖四通讯&#xff1b;②具备OTG功能&#xff0c;可适配大部分USB接口设备&#xff1b; 二、电路分析以及器件作用 1、内部电路拆解 分析&am…

【分布式存储】聊一下分布式存储中分片机制

为什么需要分片 在服务端领域&#xff0c;主要特点是支撑7*24小时不间断的服务&#xff0c;而最终对各种行为会生产对应的数据&#xff0c;比如用户登陆/注册&#xff0c;发起订单交易、支付、身份验证&#xff0c;短信验证等情况都需要存储起来&#xff0c;其中包括各种各样的…

浏览器工作原理分析与首屏加载

正文 1. 页面加载时间线 我们先来一个老生常谈的面试题&#xff1a;从输入 URL 到页面加载完成的过程中都发生了什么事情&#xff1f; 这个面试题本身也是一个开放题&#xff0c;不同方向的工程师侧重也不一样。大抵的过程可以简化为&#xff1a; st>start: 输入URL e>…

WPS表格处理

wps表格中公式出来的内容如何转为纯文本 选中公式算出的结果区域&#xff0c;复制&#xff0c;在原区域上右键&#xff0c;选择性粘贴为数值&#xff0c;就转成文本了&#xff0c;当然公式也就消除了。 wps表格如何设置整列公式&#xff1f; 1、先来看看下面这个例子需做出商…

图像分割算法

文章目录 前言1. 基于区域的分割方法1.1 区域生长算法1.2 区域分裂合并算法1.3 分水岭算法1.3.1 分水岭算法原理1.3.2 opencv-python中分水岭算法的应用 2. 基于图的分割方法2.1 Grabcut图像分割 源码仓库地址 前言 图像分割是指将图像分成若干互不重叠的子区域&#xff0c;使…

如何卸载MySQL数据库以及删除所有有关信息

目录 前言 第一步 卸载mysql程序 第二步 删除安装目录的mysql项目 第三步 删除MySQL的相关注册表 第四步 删除C盘下的 C:\ProgramData\MySQL所有的文件 第五步 删除 C:\Documents and Settings\All Users\Application Data\MySQL 下的文件夹 第六步 重启电脑 尾语 前言…

使用crictl pull时报错:“unknown service runtime.v1alpha2.ImageService”

如有错误&#xff0c;敬请谅解&#xff01; 此文章仅为本人学习笔记&#xff0c;仅供参考&#xff0c;如有冒犯&#xff0c;请联系作者删除&#xff01;&#xff01; 引言&#xff1a; crictl 是 kubernetes cri-tools 的一部分&#xff0c;是专门为 kubernetes 使用 …

【详解】String、StringBuffer、StringBuilder的基本用法及区别

一、String 1.常用的输入方式 Scanner in new Scanner&#xff08;System.in&#xff09;; //输入方法一 String s1 in.next(); //in.next(): 读到空格就停止扫描&#xff08;输入&#xff09;。//输入方法二 String s2 in.nextLine(); //in.nextLine():读到回车就停止扫描…

Avalon 学习系列 (一) —— 初步入门

相关链接 Avalon github地址 Avalon 快速入门 基于 Avalon的组件库-OniUI Avalon 入门教程 Avalon CDN 简介 Avalon 是司徒正美开发和维护的一款基于虚拟 DOM 与属性劫持的迷你、 易用、 高性能 的 前端 MVVM 框架&#xff0c;最早发布于2012.09.15。 其拥有超优秀的兼容性&a…

Python中的模块包第三方库详解

模块&包 模块 一个.py文件就是一个模块&#xff0c;里面是一些函数和变量&#xff0c;需要的时候可以导入。 模块命名规范: 1.以英文开头&#xff0c;不出现中文 2.模块名不应与系统内置函数重名 包 包本身就是一个文件夹&#xff0c;如果文件夹内有__init__.py文件&…

浅谈iic时序

一、iic通信核心要点 在iic通信中&#xff0c;进行数据传输的时候&#xff0c;遵循在scl时钟线高的时候保持sda电平稳定&#xff0c;这个电平值就是要写入的值&#xff0c;然后&#xff0c;在scl时钟线拉低的时候去改变sda上的值&#xff0c;达到自己想要输出的值 所以说&…

如何做架构设计

1、设计很重要 我们可以看一下周边的事物&#xff0c;那些好的东西&#xff0c;他们并不会天然存在&#xff0c;都是被设计出来的&#xff0c;因此设计就是创造和改善事物的重要过程。设计的重要之处在于&#xff0c;最初的设计往往决定最终的结果&#xff0c;甚至决定着事物的…

端口扫描伪装技术实践

端口扫描伪装技术实践 1.-f&#xff08;分段传输&#xff09;2.-mtu&#xff08;使用指定的MTU&#xff09;3.-D&#xff08;使用诱饵主机隐蔽扫描&#xff09;4.--source-port&#xff08;源端口欺骗&#xff09;5.--data-length &#xff08;发送报文时附加随机数据&#xff…

入职字节外包一个月,我离职了····

有一种打工人的羡慕&#xff0c;叫做“大厂”。 真是年少不知大厂香&#xff0c;错把青春插稻秧。 但是&#xff0c;在深圳有一群比大厂员工更庞大的群体&#xff0c;他们顶着大厂的“名”&#xff0c;做着大厂的工作&#xff0c;还可以享受大厂的伙食&#xff0c;却没有大厂…

电子元器件解析02之电容(二)——电容分类与应用场景

书接上文&#xff1a;电子元器件解析02之电容(一)——定义与性能参数&#xff1a;https://blog.csdn.net/weixin_42837669/article/details/131142286 摘要 本文总结了各种不同介质电容的特性&#xff0c;包括陶瓷电容、电解电容、薄膜电容等&#xff1b;同时对一些特殊场合的电…

Hadoop Distributed System (HDFS) 写入和读取流程

一、HDFS HDFS全称是Hadoop Distributed System。HDFS是为以流的方式存取大文件而设计的。适用于几百MB&#xff0c;GB以及TB&#xff0c;并写一次读多次的场合。而对于低延时数据访问、大量小文件、同时写和任意的文件修改&#xff0c;则并不是十分适合。 目前HDFS支持的使用…

HTTP协议【网络基础/应用层】

文章目录 1. 网络基础 TCP/IP2. 与HTTP密切相关的协议2.1 负责传输的 IP 协议路由选择 2.2 确保可靠性的 TCP 协议三次握手 2.3 负责域名解析的 DNS 服务2.4 各种协议和 HTTP 协议的关系 3. URL和编码问题3.1 介绍格式 3.2 编码问题 4. 初识 HTTP 协议4.1 C-S 模式4.2 通过响应…