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

2,693 阅读6分钟

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

一、前言

在项目初期,为了统一管理列表的分页加载逻辑,于是写了一个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

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

3. Pager

属性类型备注
pageConfigPageConfig跟分页相关的基础配置
属性:
initIndex:首页页码
initSize:每页的条数
prevIndex:上一页页码
nextIndex:下一页页码
方法:
reset() --- 重置为初始数据
requestsuspend (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)
      }
    }