犹记得初次接触paging时,数据源DataSource,分为positional/ItemKeyed/pageKeyed DataSource,长处是严厉把控数据源,削减过错和溃散,但缺点也是把控太严厉,并且许多定制化需求不支撑,如header和footer,完成起来太过费事,所以扔掉了。 paging3.x现在更新了
pagingSource
,remoteMediator
和insertSeparators
等优异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
- getRefreshKey便是当咱们恳求刷新时的key,这儿HelloWorld先回来null
- 重点先了解load这个函数:
params.key
:便是你自行传递的恳求下一页的key,初次恳求是null值,自行写初始恳求key,这儿是0; -
params.loadSize
:便是你一页恳求的个数
初次恳求时,loadSize是含有预加载个数的,默许是你后边配的pageSize*3)
- 分页成功后回来
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。初次尝试,会发现过程比自己写的繁琐些。但用到实践事务,会发现扩展性强大,写起事务场景很流畅。
三 占位符PlaceHolder
3.1 概念相关
还记得前面讲到的,adapter烘托数据时拿到的数据message是可空值么?便是由于他是回来了占位符,数据传null
class SimpleViewHolder(parent: ViewGroup):xxx {
...
@SuppressLint("SetTextI18n")
fun bindTo(msg: Message?) { //可空
...
}
}
先上两张大佬的动图了解下差异,
先看下未敞开占位符的:
没有敞开占位符的情况下,就更惯例的加载更多相同,滑到最底部就阻塞等待加载,即列表展示的是当前所有的数据,留意后侧滚动条,当滚动到列表底部,成功加载下一页数据后,滚动条会从长变短,也便是新数据来了,列表变成。 也便是说为敞开占位符时,recycleView显现的条目个数==已经获取的数据总个数
敞开了占位符之后,
当用户滑动到了底部尚未加载的数据时,开发者会看到还未烘托的条目。此时adpter在onBindViewHolder获取的数据是null
,由咱们开发者自行在决议未有真实时item如何烘托! 一般是保持一致高度,这样有真实数据来的时分就不会发生视觉差。
3.2 代码敞开占位符功用
代码->PlaceholdersFragment
占位符功用敞开相对简略,
- 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)
- 在数据源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
)
- adapter自行兼容item数据为null,即占位符时烘托;
Adapter烘托占位符
//step 3 null时显现content为 "我是占位符"
contentView.text = msg?.content ?: "我是占位符"
idView.text = msg?.id?.let { "ID:$it" } ?: "----"
四 数据改换:过滤、列表分隔符、header,Footer
4.1 功用概述
官方文档:转化数据流 代码->InsertSeparatorsFragment
转化数据流是比较常见的操作,比方将网络Response转成uiModel,过滤,刺进分隔符等;paging3开发了少数的操作符允许数据改换:
pager.flow
.map { pagingData ->
pagingData.filter { xxx }
.insertSeparators {xxx}
...
}
.cachedIn(viewModelScope)
paging库将数据源尽可能躲藏,确保了数据安全不变性,但某些场景却加大了开发者扩展耗时。个人比较喜欢在
pagingSource
,发生数据源的时分就按场景调整好数据。刺进列表符就用paging供给的insertSeparators
操作符
过滤和map简略了解就不讲了,讲讲insertSeparators
,依据前后两个item刺进分隔符,header/footer
算是前/后item为null的特殊分隔符。大概效果如:
留意白色item为刺进的header,分隔符和footer,每个十个item刺进一个分隔符
能够了解为一种多类型支撑吧。
4.2 代码完成
中心在于了解insertSeparators
能做什么,代码改动很简略,大致跟界说多类型布局相同。
- 界说多一个切割符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
}
}
- 改换数据 对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已经开放了一些操作符满足了上述的需求。
五 后续
本想一次性写完,发现有点啰嗦,篇幅长了,就将下述的切到第二篇吧。下一份主要着重讲两大块功用:
- 状况功用:在正常的事务开发里,完善的界面是有状况的,loading -> success/error -> retry -> refresh。每一个状况对应不同ui展示。paging3是支撑状况操控和监听的。
许多开发者偷懒,就简略处理了加载和成功,根本不考虑异常状况,一出现异常各种溃散还排查不到问题所在,因小失大。
-
本地数据库和网络数据结合:paging3供给了
remoteMediator(实验性api)
和Room扩展类,能快速支撑开发者从本地数据库和网络加载分页数据
本文demo已放到git仓库
六 ❤️ 感谢
假如觉得这篇内容对你有所帮助,一键三连支撑下()
关于纠错和主张:欢迎直接在留言分享记录()