Android端使用200行代码实现的分页加载 — PageHelper

Android端使用200行代码实现的分页加载 — PageHelper以`RefreshLayout`和`BaseQuickAdapter`为基础的分页加载组件,在使用的时候,只需要对相关的数据进行配置即可,不需要实现任何的逻辑代码。

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、前言

在项目初期,为了统一管理列表的分页加载逻辑,于是写了一个BaseRvActivity/BaseRvFragment去实现。后来随着项目的不断庞大,渐渐地往里面增加了单选/多选,筛选条件配置等等其他的逻辑,甚至于还加入了一些业务相关的逻辑代码,并且还继承于这两个文件实现了几个扩展后的BaseXxActivity/BaseXxFragment,导致某些页面的Activity变成了BaseActivity的第N代子孙,继承关系就成了 MouYeWuActivity -> BaseYwActivity -> BaseXxxxActivity -> BaseXxxActivity -> BaseXxActivity -> BaseActivity 这样。

这是我们想看到的吗?不,并不是。

在这样的情境下,突然想到了最近在学习Jetpack Compose时看到的一句话,“用组合代替了继承”,显然这个概念并不是我第一次遇到,只是一直没有引起重视罢了。这时想起,在Jetpadk中的Paging不就是这样的一个库吗?于是开始翻Paging3的文档照着写实例,最终发现这个库十分贴切地表达了“组合”的思想。

但是,问题来了,之前项目中一直使用的是BaseQuickAdapter作为适配器,但是Paging3用的确实RecyclerView.Adapter的一个子类PagingDataAdapter,如果要将Paging3引入项目,项目中如果不想存在两套分页框架,那么就必须将之前所有的列表适配器全部替换掉,这个工程可不小,而且也不是我们愿意看到的。

怎么办呢?那还能怎么办,自己撸一个呗。于是,这200行代码诞生了。

二、接下来,先看一下效果

下面的效果均模拟了耗时2秒的网络请求

  1. 模拟第3页为最后一页的效果 GIF 2022-5-21 1-08-05.gif

  2. 模拟第3页加载出现异常的效果

GIF 2022-5-21 1-28-07.gif

三、PageHelper 介绍

RefreshLayoutBaseQuickAdapter为基础的分页加载组件,整个组件包含PageHelperPagerPageUiLoadState四个部分。在使用的时候,只需要对相关的数据进行配置即可,不需要实现任何的逻辑代码。

  1. LoadState:定义加载状态
  2. PageUi:负责注册相关的UI组件
  3. Pager:包含分页数据和数据源以及相关状态的管理
  4. PageHelper:调用者直接使用的文件,负责了UI组件相关逻辑的初始化

1. LoadState

包含3个子类 描述 备注
NotLoading 无加载状态,可作为加载完成的状态处理 包含一个noMore属性,表示是否已经没有更多数据。可在接收到该状态时进行相关操作。
在其他两个状态中也有这个属性,但是在目前看起来并没有什么用。
Loading 加载中状态
Error 加载异常状态,在获取数据过程中出现异常时回调 包含一个error属性,为在获取数据过程中发生的异常信息,可在接收到该状态时,利用该属性执行相应的异常处理逻辑。

2. PageUi

属性 类型 备注
scope kotlinx.coroutines.CoroutineScope 建议在View层使用,传入lifecycleScope
虽然在ViewModel层中传入viewModelScope也是可以的,但是不推荐这样用。
refreshLayout com.scwang.smart.refresh.layout.api.RefreshLayout 第三方的上拉下拉组件 adapter com.chad.library.adapter.base.BaseQuickAdapter 第三方的列表数据适配器组件

3. Pager

属性 类型 备注
pageConfig PageConfig 跟分页相关的基础配置
属性:
initIndex:首页页码
initSize:每页的条数
prevIndex:上一页页码
nextIndex:下一页页码
方法:
reset() — 重置为初始数据
request suspend (Int, Int) -> List 获取数据的方法,第一个Int参数为页码page,第二个Int参数为每页的条数pageSize

4. PageHelper

属性 描述
pageUi 负责注册相关的UI组件
pager 包含分页数据和数据源以及相关状态的管理

公开的方法:

方法 描述
fun refresh() 代码控制刷新
suspend fun requestNext() 代码控制获取下一页

四、如下是上面效果图的主要代码

提前说明一下,主要的管理逻辑其实都是在Pager类中实现的。因此,如果分页的效果要用在其他的控件上,只需要将PageUi中的属性类型替换掉,然后在PageHelper对控件实现相应的初始化逻辑即可。

class SimplePageActivity : AppCompatActivity() {
  
  ......

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

    // 1. 创建PageUi,指定UI组件
    val pageUi = PageUi(
      scope = lifecycleScope,
      refreshLayout = refreshLayout,
      adapter = adapter
    )

    // 2. 创建Pager,定义分页配置,指定获取数据的方法,设置监听器
    val pager = Pager(pageConfig = PageConfig(initIndex = 1),
      request = { page, size -> SimpleHttpService.requestList(page = page, pageSize = size) }).apply {
      // 设置是否没有更多数据的状态监听器
      setNoMoreStateListener(object : PageState.NoMoreStateListener {
        override fun noMoreState(noMoreState: Boolean) {
          Log.e("SimplePageActivity", "noMoreState = $noMoreState")
        }
      })
      // 添加加载状态监听器
      addLoadStateListener(object : PageState.LoadStateListener {
        override fun updateState(loadState: LoadState) {
          when (loadState) {
            is LoadState.NotLoading -> {
              hideLoading()
              showToast("加载完成")
            }
            is LoadState.Loading -> {
              showLoading("加载中")
            }
            is LoadState.Error -> {
              hideLoading()
              errorDialog.setMessage(loadState.error.message)
              errorDialog.show()
              showToast("加载失败,${loadState.error.message}")
            }
          }
        }
      })
    }

    // 3. 创建PageHelper,将pageUi和pager联系起来
    pageHelper = PageHelper<String>(pageUi = pageUi, pager = pager)

    // 4. 通过代码进行刷新操作
    pageHelper.refresh()
  }

  ......
  
}

五、附上源码

  1. LoadState

    sealed class LoadState(val noMore: Boolean) {
      /** * 无加载状态,可作为加载完成的状态处理 */
      class NotLoading(noMore: Boolean) : LoadState(noMore) {
        override fun toString(): String {
          return "NotLoading(noMore=$noMore)"
        }
    
        override fun equals(other: Any?): Boolean {
          return other is NotLoading &&
              noMore == other.noMore
        }
    
        override fun hashCode(): Int {
          return noMore.hashCode()
        }
      }
    
      /** * 加载中状态 */
      object Loading : LoadState(false) {
        override fun toString(): String {
          return "Loading(noMore=$noMore)"
        }
    
        override fun equals(other: Any?): Boolean {
          return other is Loading &&
              noMore == other.noMore
        }
    
        override fun hashCode(): Int {
          return noMore.hashCode()
        }
      }
    
      /** * 加载异常状态,在获取数据过程中出现异常时回调 */
      class Error(
        val error: Throwable
      ) : LoadState(false) {
        override fun equals(other: Any?): Boolean {
          return other is Error &&
              noMore == other.noMore &&
              error == other.error
        }
    
        override fun hashCode(): Int {
          return noMore.hashCode() + error.hashCode()
        }
    
        override fun toString(): String {
          return "Error(noMore=$noMore, error=$error)"
        }
      }
    }
    
  2. PageUi

    class PageUi<T>(
      val scope: CoroutineScope,
      val refreshLayout: RefreshLayout,
      val adapter: BaseQuickAdapter<T, *>
    )
    
  3. Pager

    class Pager<T>(
      internal val pageConfig: PageConfig,
      internal val request: suspend (Int, Int) -> List<T>
    ) {
      internal val pageState: PageState<T> = PageState()
    
      /** * 设置是否没有更多数据的状态监听器 */
      fun setNoMoreStateListener(noMoreStateState: PageState.NoMoreStateListener) {
        pageState.setNoMoreStateListener(noMoreStateListener = noMoreStateState)
      }
    
      /** * 添加加载状态监听器 */
      fun addLoadStateListener(loadStateState: PageState.LoadStateListener) {
        pageState.addLoadStateListener(loadStateListener = loadStateState)
      }
    
      /** * 发送没有更多数据的状态 * * 如果外部需要调用此方法,请使用[PageHelper.callNoMoreState] */
      internal fun callNoMoreState(noMoreState: Boolean) {
        pageState._noMoreStateListener?.noMoreState(noMoreState = noMoreState)
      }
    
      /** * 刷新(重新获取第一页) */
      internal suspend fun refresh(): List<T> {
        pageState.callLoadState(loadState = LoadState.Loading)
        pageConfig.reset()
        val result = request.runCatching { invoke(pageConfig.index.run { pageConfig.index++ }, pageConfig.size) }
        return checkResult(result = result)
      }
    
      /** * 获取下一页 */
      internal suspend fun requestNext(): List<T> {
        pageState.callLoadState(loadState = LoadState.Loading)
        val result = request.runCatching { invoke(pageConfig.nextIndex.run { pageConfig.index++ }, pageConfig.size) }
        return checkResult(result = result)
      }
    
      /** * 查验请求的结果 */
      private fun checkResult(result: Result<List<T>>): List<T> {
        val list = if (result.isSuccess) {
          val _list = result.getOrNull() ?: emptyList()
          pageState.checkPageList(_list, pageConfig.size)
          pageState.callLoadState(loadState = LoadState.NotLoading(pageState.noMore))
          _list
        } else {
          pageConfig.index--  //因为之前在请求数据时,取了页码值后立即对页码进行了+1,因此如果请求数据失败,则需要通过-1将页码进行回退
          val throwable = result.exceptionOrNull() ?: Throwable("获取数据失败")
          pageState.callLoadState(loadState = LoadState.Error(throwable))
          throwable.printStackTrace()
          emptyList()
        }
        return list
      }
    }
    
    /** * 基础配置 * * @property initIndex 首页页码 */
    class PageConfig(private val initIndex: Int, initSize: Int = 20) {
      var index: Int = initIndex
      var size: Int = initSize
    
      //上一页页码
      val prevIndex: Int? get() = if (index > initIndex) index - 1 else null
    
      //下一页页码
      val nextIndex: Int get() = index + 1
    
      /** * 重置数据 */
      fun reset() {
        index = initIndex
      }
    }
    
    /** * 所有的状态管理器 */
    class PageState<T> {
      //是否没有更多数据的状态
      val noMoreFlow = MutableStateFlow(false)
    
      val noMore: Boolean get() = noMoreFlow.value
    
      /** * 检查获取到的数据 * * 计算出是否还有下一页 */
      fun checkPageList(list: List<T>, pageSize: Int) {
        val _noMore = list.size < pageSize
        if (noMoreFlow.value != _noMore) {
          noMoreFlow.value = _noMore
        }
      }
    
      /*---------- 是否没有更多数据的状态 ----------*/
    
      private var noMoreStateListener: NoMoreStateListener? = null
    
      val _noMoreStateListener: NoMoreStateListener? get() = noMoreStateListener
    
      fun setNoMoreStateListener(noMoreStateListener: NoMoreStateListener) {
        this.noMoreStateListener = noMoreStateListener
      }
    
      /** * 是否没有更多数据的状态的回调接口 */
      interface NoMoreStateListener {
        fun noMoreState(noMoreState: Boolean)
      }
    
      /*---------- 加载状态 ----------*/
    
      private val loadStateListeners = mutableSetOf<LoadStateListener>()
    
      fun addLoadStateListener(loadStateListener: LoadStateListener) {
        loadStateListeners.add(loadStateListener)
      }
    
      fun removeLoadStateListener(loadStateListener: LoadStateListener) {
        loadStateListeners.remove(loadStateListener)
      }
    
      /** * 发送加载状态 * * @param loadState 新的状态 */
      fun callLoadState(loadState: LoadState) {
        loadStateListeners.forEach {
          it.updateState(loadState = loadState)
        }
      }
    
      /** * 加载状态回调接口 */
      interface LoadStateListener {
        fun updateState(loadState: LoadState)
      }
    }
    
  4. PageHelper

    class PageHelper<T>(
      val pageUi: PageUi<T>,
      val pager: Pager<T>,
    ) {
      init {
        pageUi.refreshLayout.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
          override fun onRefresh(refreshLayout: RefreshLayout) {
            pageUi.scope.launch {
              val list = pager.refresh()
              pageUi.adapter.setList(list)
              if (pager.pageState.noMore) {
                refreshLayout.finishRefreshWithNoMoreData()
              } else {
                refreshLayout.finishRefresh()
              }
            }
          }
    
          override fun onLoadMore(refreshLayout: RefreshLayout) {
            pageUi.scope.launch {
              val list = pager.requestNext()
              pageUi.adapter.addData(list)
              if (pager.pageState.noMore) {
                refreshLayout.finishLoadMoreWithNoMoreData()
              } else {
                refreshLayout.finishLoadMore()
              }
            }
          }
        })
    
        pageUi.scope.launch {
          pager.pageState.noMoreFlow.collectLatest {
            pager.callNoMoreState(noMoreState = it)
          }
        }
      }
    
      /** * 代码控制刷新 */
      fun refresh() {
        pageUi.refreshLayout.autoRefresh()
      }
    
      /** * 代码控制获取下一页 */
      suspend fun requestNext() {
        pager.requestNext()
      }
    
      /** * 发送没有更多数据的状态 * * 实验性api,不确定在实际使用中的逻辑效果是否能满足实际需求 */
      @ExperimentalStdlibApi
      fun callNoMoreState(noMoreState: Boolean) {
        pager.callNoMoreState(noMoreState = noMoreState)
      }
    }
    

今天的文章Android端使用200行代码实现的分页加载 — PageHelper分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22499.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注