犹记得初次接触paging时,数据源DataSource,分为positional/ItemKeyed/pageKeyed DataSource,长处是严厉把控数据源,削减过错和溃散,但缺点也是把控太严厉,并且许多定制化需求不支撑,如header和footer,完成起来太过费事,所以扔掉了。 paging3.x现在更新了pagingSourceremoteMediatorinsertSeparators等优异api,从头界说写法,大大提高了可扩展性。所以从头了解其规划理念和实践使用,向Google工程师学习其优异的结构规划能力。

Paging3 官方分页库拆解与使用

一 前言

Paging是jetpack的一个分页组件,能够使开发者更轻松在 RecyclerView中分页加载来自本地存储或网络端数据。包含一些特性支撑:

  • 供给了配套的RecyclerView适配器,会在用户滚动到已加载数据的“末端”时主动恳求数据「即主动提前向下/向上加载更多」
  • 本地数据库Room供给了快捷支撑扩展
  • 对协程flow,liveData以及rxjava这几个常用的呼应式开发api有一流的支撑
  • 包含过错重试,刷新等功用;

1.1 一些材料

官方材料永远是最正确的,能确保实时更新。现在的remoteMediator仍是实验性,后续更新随时都会改动api调用方法。所以本文也只能从全体结构和规划上给大家指指路,具体api得按最新的来。

  • 官方文档:Paging库概述
  • “Android Paging 基础知识”Codelab
  • “Android Paging 高档知识”Codelab
  • 优异的博客「基于旧版本写的,可供参考」:反思|Android 列表分页组件Paging的规划与完成:系统概述

个人习惯是,在开始正式学习前,先找好对应的官方及优异的第三方材料&demo,好的材料支撑能事半功倍,甚至有意外收获。

二 快速上手

2.1 库引进

paging本身已经支撑协程和flow。

dependencies {
  def paging_version = "3.1.1"
  implementation "androidx.paging:paging-runtime:$paging_version"
  // 可选 - RxJava2 支撑
  implementation "androidx.paging:paging-rxjava2:$paging_version"
  // 可选 - RxJava3 支撑
  implementation "androidx.paging:paging-rxjava3:$paging_version"
  // 可选 - Room 对paging的扩展支撑
  def room_version = "2.5.0"
  implementation "androidx.room:room-paging:$room_version"
	xxx
}

2.2 HelloWorld!

为了削减阅览本钱,每个demo的fragment尽可能是独立的完整代码,不加封装;后续功用扩展都是基于这个简易demo,标识step1 2 3便利了解阅览

代码->HelloWorldFragement

2.2.1 界说简略数据模型Message,layout文件便是两个TextView就不写了。

data class Message
//数据模型
data class Message(
    val id: Int,
    val content: String,
)

2.2.2 界说RecyclerView.Adapter

paging3供给了PagingDataAdapter,只需供给承继并供给一个DiffUtil.ItemCallback即可。

DiffUtil.ItemCallback是一个高效辅助工具,大家没用过的话先简略查下,很容易了解

PagingDataAdapter & DiffUtil.ItemCallback
class SimpleAdapter : PagingDataAdapter<Message, SimpleViewHolder>(diffCallback) {
    override fun onBindViewHolder(holder: SimpleViewHolder, position: Int) {
        holder.bindTo(getItem(position))
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleViewHolder =
        SimpleViewHolder(parent)
    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<Message>() {
            override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean =
                oldItem.id == newItem.id
            override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean =
                oldItem == newItem
        }
    }
}
class SimpleViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.item_message_content, parent, false)) {
    private val contentView get() = itemView.findViewById<TextView>(R.id.tvContent)
    private val idView get() = itemView.findViewById<TextView>(R.id.tvId)
    private var msg: Message? = null
    @SuppressLint("SetTextI18n")
    fun bindTo(msg: Message?) {
        this.msg = msg
        contentView.text = msg?.content
        idView.text = "ID:${msg?.id?.toString() ?: "0"}"
    }
}

有没有留意到,在这儿的bindTo方法传入的message是可空类型?,这儿留到后边placeHolder是一并解说。

2.2.3 界说数据源 PagingSource<Key : Any, Value : Any>


class SimplePagingSource(
    private val service: FakeService
) : PagingSource<Int, Message>() {
    override fun getRefreshKey(state: PagingState<Int, Message>): Int? {
        return null
    }
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
        val key = params.key ?: 0
        val nextKey = key + params.loadSize
        val newData = service.fetchData(key.until(nextKey))
        return LoadResult.Page(
            data = newData,
            prevKey = null,
            nextKey = if (nextKey >=100) null else nextKey
        )
    }
}

这儿简略了解咱们的分页逻辑是,按item的index来恳求, 10-20便是恳求第10-20个item的意思,最多设置恳求100 因而PagingSource<Key : Any, Value : Any>的key便是Int,value则是咱们的数据模型Message

  1. getRefreshKey便是当咱们恳求刷新时的key,这儿HelloWorld先回来null
  2. 重点先了解load这个函数: params.key:便是你自行传递的恳求下一页的key,初次恳求是null值,自行写初始恳求key,这儿是0;
  3. params.loadSize:便是你一页恳求的个数

初次恳求时,loadSize是含有预加载个数的,默许是你后边配的pageSize*3)

 Paging3  官方分页库拆解与应用(上)

  1. 分页成功后回来LoadResult.Page:data便是新的分页items,pre/nextkey是要害。 nextKey是惯例列表的向下(即手势向上)是否还能加载更多,null表示没有更多,这儿设置最多是100个if (nextKey >=100) null else nextKey; preKey则是向上是否能加载更多,比方咱们的微信聊天信息界面,一般是列表向上(即手势向下)看看是否有更多的前史聊天信息。

2.2.4 装备pager

现在有了数据源了,接下来便是简略装备PagingConfig,即你每一页要恳求多少个数据pageSize,是否预加载等等; 这儿咱们在viewModel简略装备一页恳求10个数据

PagingConfig
val flow = Pager(
        config = PagingConfig(pageSize = 10,initialLoadSize = 30),
        pagingSourceFactory = { SimplePagingSource(FakeService()) }
    )
        .flow
        .cachedIn(viewModelScope)

这儿的initialLoadSize便是第一次加载时的个数,默许是你传入的pageSize的三倍。这儿写出来是为了让你知道,前面loadParams.loadSize在第一次恳求时是==initialLoadSize,后边的恳求才==pageSize。规划上是很正确有意义的,惋惜在实践开发中得承认后台逻辑是否能够兼容不同pageSize。避免不一致踩坑。

后台的分页逻辑千奇百怪,见怪不怪。。。

2.2.5 监听数据并提交给adapter

这儿最终一步便是监听pager的flow并传给我的界说的PagingDataAdapter即可

监听数据并烘托
simpleViewModel.flow
                .collectLatest {
                    adapter.submitData(it)
                }

ok,到这儿就完成了简略的helloWorld。初次尝试,会发现过程比自己写的繁琐些。但用到实践事务,会发现扩展性强大,写起事务场景很流畅。

 Paging3  官方分页库拆解与应用(上)

三 占位符PlaceHolder

3.1 概念相关

还记得前面讲到的,adapter烘托数据时拿到的数据message是可空值么?便是由于他是回来了占位符,数据传null

class SimpleViewHolder(parent: ViewGroup):xxx {
    ...
    @SuppressLint("SetTextI18n")
    fun bindTo(msg: Message?) { //可空
        ...
    }
}

先上两张大佬的动图了解下差异,

先看下未敞开占位符的:

 Paging3  官方分页库拆解与应用(上)

没有敞开占位符的情况下,就更惯例的加载更多相同,滑到最底部就阻塞等待加载,即列表展示的是当前所有的数据,留意后侧滚动条,当滚动到列表底部,成功加载下一页数据后,滚动条会从长变短,也便是新数据来了,列表变成。 也便是说为敞开占位符时,recycleView显现的条目个数==已经获取的数据总个数

敞开了占位符之后,

 Paging3  官方分页库拆解与应用(上)

当用户滑动到了底部尚未加载的数据时,开发者会看到还未烘托的条目。此时adpter在onBindViewHolder获取的数据是null,由咱们开发者自行在决议未有真实时item如何烘托! 一般是保持一致高度,这样有真实数据来的时分就不会发生视觉差。

3.2 代码敞开占位符功用

代码->PlaceholdersFragment

占位符功用敞开相对简略,

  1. viewModel的Pager装备中敞开占位符功用enablePlaceholders=true
PagingConfig:enablePlaceholders=true
val flow = Pager(
        //step 1 敞开占位符功用:enablePlaceholders = true
        config = PagingConfig(pageSize = 10, enablePlaceholders = true),
        pagingSourceFactory = { PlaceholderPagingSource(FakeService()) }
    )
        .flow
        .cachedIn(viewModelScope)
  1. 在数据源PagingSource中自行决议界面要显现多少个占位符itemsAfter/itemsBefore,这儿是向下,所以只需itemsAfter写10个, LoadResult.Page(xxx,itemsAfter = 10)
PagingSource:itemsAfter = 10
return LoadResult.Page(
            data = newData,
            prevKey = null,
            nextKey = if (nextKey >=100) null else nextKey,
            //step 2 刺进10个占位符
            itemsAfter = 10
        )
  1. adapter自行兼容item数据为null,即占位符时烘托;
Adapter烘托占位符
//step 3 null时显现content为 "我是占位符"
contentView.text = msg?.content ?: "我是占位符"
idView.text = msg?.id?.let { "ID:$it" } ?: "----"

 Paging3  官方分页库拆解与应用(上)

四 数据改换:过滤、列表分隔符、header,Footer

4.1 功用概述

官方文档:转化数据流 代码->InsertSeparatorsFragment

转化数据流是比较常见的操作,比方将网络Response转成uiModel,过滤,刺进分隔符等;paging3开发了少数的操作符允许数据改换:

pager.flow
  .map { pagingData ->
    pagingData.filter { xxx }
		    .insertSeparators {xxx}
	      ...
  }
	.cachedIn(viewModelScope)

 Paging3  官方分页库拆解与应用(上)

paging库将数据源尽可能躲藏,确保了数据安全不变性,但某些场景却加大了开发者扩展耗时。个人比较喜欢在pagingSource,发生数据源的时分就按场景调整好数据。刺进列表符就用paging供给的insertSeparators操作符

过滤和map简略了解就不讲了,讲讲insertSeparators,依据前后两个item刺进分隔符,header/footer算是前/后item为null的特殊分隔符。大概效果如:

留意白色item为刺进的header,分隔符和footer,每个十个item刺进一个分隔符

 Paging3  官方分页库拆解与应用(上)

能够了解为一种多类型支撑吧。

4.2 代码完成

中心在于了解insertSeparators能做什么,代码改动很简略,大致跟界说多类型布局相同。

  1. 界说多一个切割符data class以及更新DiffUtil.ItemCallback
分隔符多布局
// step 1.1 界说不同viewType model
data class SeparatorModel(val desc: String)
// step 1.2 界说diffCallback
private val diffCallback = object : DiffUtil.ItemCallback<Any>() {
    override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
        return (oldItem is Message && newItem is Message && oldItem.id == newItem.id)
                || (oldItem is SeparatorModel && newItem is SeparatorModel && oldItem.desc == newItem.desc)
    }
    override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean =
        (oldItem is Message && newItem is Message && oldItem == newItem)
                || (oldItem is SeparatorModel && newItem is SeparatorModel && oldItem == newItem)
}
// step 1.3 界说多类型viewHolder和adapter
class SeparatorViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.item_message_separator, parent, false)
) {
    private val separatorTv get() = itemView.findViewById<TextView>(R.id.tvSeparator)
    @SuppressLint("SetTextI18n")
    fun bindTo(model: SeparatorModel) {
        separatorTv.text = model.desc
    }
}
  1. 改换数据 对viewModel中pager.flow进行改换
数据改换
val flow = Pager(
        config = PagingConfig(pageSize = 10),
        pagingSourceFactory = { InsertSeparatorPagingSource(FakeService()) }
    )
        .flow
        //step 2 刺进header,footer,惯例切割符
        .map { pagingData ->
            pagingData.insertSeparators<Message, Any> { before, after ->
                when {
                    //前面item为null,代表刺进的是header
                    before == null -> SeparatorModel("我是header,ID:0->9")
                    //后边item为null,代表刺进的是footer
                    after == null -> SeparatorModel("我是Footer,Goodbye!!!")
                    //每隔十个刺进一个分隔符
                    before.id % 10 != 0 && after.id % 10 == 0 -> SeparatorModel("ID:${after.id}->${after.id + 9}")
                    else -> null
                }
            }
        }.cachedIn(viewModelScope)

是的,这就完成了分隔符功用了。

还记得paging2.x的时分不支撑这些转化,刺进header和footer得自己去改造adapter,特别费事。现在paging3的pagingData已经开放了一些操作符满足了上述的需求。

五 后续

本想一次性写完,发现有点啰嗦,篇幅长了,就将下述的切到第二篇吧。下一份主要着重讲两大块功用:

  1. 状况功用:在正常的事务开发里,完善的界面是有状况的,loading -> success/error -> retry -> refresh。每一个状况对应不同ui展示。paging3是支撑状况操控和监听的。

 Paging3  官方分页库拆解与应用(上)

 Paging3  官方分页库拆解与应用(上)

许多开发者偷懒,就简略处理了加载和成功,根本不考虑异常状况,一出现异常各种溃散还排查不到问题所在,因小失大。

  1. 本地数据库和网络数据结合:paging3供给了remoteMediator(实验性api)和Room扩展类,能快速支撑开发者从本地数据库和网络加载分页数据

本文demo已放到git仓库

六 ❤️ 感谢

假如觉得这篇内容对你有所帮助,一键三连支撑下()

关于纠错和主张:欢迎直接在留言分享记录()