Paging3简介
Paging是一个分页库,是Google定义并实现的一套加载数据并显示的方案。从官方库中可以看到其重新定义了一个PagingDataAdapter,并以次为入口实现了网络加载和UI展示相关的逻辑。这些在原本的RecyclerView+Adapter模式中均是由业务开发者实现的,现在Google帮我们实现了,大大节约了开发成本,并且质量也有一定保证。
主要优点
1.在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。
2.提供数据去重功能,可以减少数据冗余并节约系统资源。
3.提供数据预取功能,保证下拉刷新流畅。
4.提供加载数据时加载样式header和footer的自定义功能。
5.提供加载错误以及重试的功能支持。
6.支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava
注意事项
1.PagingConfig中maxSize有最小值限制,设置maxSize后只会缓存maxSize个数据,因此可能带来历史数据擦除问题,导致回显历史数据异常。
2.Paging必须限制服务端数据提供用于区分页码的唯一标示。
3.Paging从PagingDataAdapter的getItemCount得到的值有可能并不是真正的数据量,因为其中包含了PlaceHolder的个数。
4.Paging中RemoteMediator方案是先获取网络数据然后再缓存到Room数据库,UI展示的数据均从数据库中获取,这样相当于数据获取两次才到达UI展示这一步,是否一定要这样实现?
5.Paging中RemoteMediator方案中下拉一定页数后,新的一批数据加载时回展示在首部,历史数据也从UI中消失,效果类似一下刷新操作的结果。这样产品是否会接受?
配置依赖
在相应模块的build.gradle文件下配置:
implementation deps.paging_runtime
implementation deps.retrofit.gson
implementation deps.okhttp_logging_interceptor
implementation deps.activity.activity_ktx
相应的版本配置文件为:
def versions = [:]
versions.paging = "3.0.0-alpha01"
versions.retrofit = "2.9.0"
versions.glide = "4.8.0"
versions.okhttp_logging_interceptor = "3.9.0"
versions.lifecycle = "2.2.0"
versions.activity = '1.1.0'
versions.kotlin = "1.3.72"
def deps = [:]
deps.paging_runtime = "androidx.paging:paging-runtime:$versions.paging"
ext.deps = deps
def retrofit = [:]
retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
deps.retrofit = retrofit
def glide = [:]
glide.runtime = "com.github.bumptech.glide:glide:$versions.glide"
glide.compiler = "com.github.bumptech.glide:compiler:$versions.glide"
deps.glide = glide
deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"
def activity = [:]
activity.activity_ktx = "androidx.activity:activity-ktx:$versions.activity"
deps.activity = activity
网络仓库
在Paging中要求一个网络仓库主要包含一个Pager对象,它是Paging的主要入口点,它构建了一条可交互的PagingData流,Pager源码很简单,看一下:
class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
config: PagingConfig,
initialKey: Key? = null,
@OptIn(ExperimentalPagingApi::class)
remoteMediator: RemoteMediator<Key, Value>? = null,
pagingSourceFactory: () -> PagingSource<Key, Value>
) {
/**
* A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become
* invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or
* [PagingDataAdapter.refresh].
*/
val flow: Flow<PagingData<Value>> = PageFetcher(
pagingSourceFactory,
initialKey,
config,
remoteMediator
).flow
}
即构造一个PageFetcher对象,并将其的流对象flow赋值给自己的flow成员。Pager主要包含两个对象:接口参数PagingConfig、数据源PagingSource。下面是网络仓库代码:
class InMemoryByPageKeyRepository(private val redditApi: RedditApi) : RedditPostRepository {
override fun postsOfSubreddit(subReddit: String, pageSize: Int) = Pager(
PagingConfig(pageSize)
) {
PageKeyedSubredditPagingSource(
redditApi = redditApi,
subredditName = subReddit
)
}.flow
}
1.PagingConfig
PagingConfig是用于描述怎么样从PagingSource加载内容的,包括页大小(必须)、初始页大小、预取距离、placeholder开关等,其中的字段如图:
2.PagingSource
PagingSource的结构图如图所示:
其中最主要的是一个虚方法onLoad()、LoadParams内部类、LoadResult内部类,onLoad()方法是提供给用户重写用于从网络获取数据的,其参数是LoadParams类型,返回结果是LoadResult类型。LoadParams的结构如图:
其中成员变量key是用于区分不同页的标示,loadSize、placeholderEnabled与上文PagingConfig相对应。其中的三个继承LoadParams的内部类Append、Refresh、Prepend分别对应后项加载、刷新、前向加载三种访问网络的类型。下面看一下LoadResult的结构图:
LoadResult中有两个内部类,分别继承LoadResult,Error标示获取网络数据失败,Page标示获取网络数据成功。Page中data即为加载成功的数据列表,preKey标示上一页的key,nextKey表示下一页的key,在触发加载更多的时候nextKey又会重新被封装进一个LoadParams的key字段中,告诉服务端需要拉取的页码。源码doLoad()方法中可以看到这一过程:
private suspend fun doLoad(
scope: CoroutineScope,
state: PagerState<Key, Value>,
loadType: LoadType,
generationalHint: GenerationalViewportHint
) {
require(loadType != REFRESH) { "Use doInitialLoad for LoadType == REFRESH" }
var loadKey: Key? = stateLock.withLock {
with(state) {
generationalHint.hint.withCoercedHint { indexInPage, pageIndex, hintOffset ->
nextLoadKeyOrNull(
loadType,
generationalHint.generationId,
indexInPage,
pageIndex,
hintOffset
)?.also { setLoading(loadType, false) }
}
}
}
}
将数据流注入适配器PagingDataAdapter
通过上面几步已经构造了一个Pager对象,并可以拿到其流对象flow,下面在ViewModel中进一步使用该流对象:
class PagingViewModel(
private val repository: RedditPostRepository,
private val saveHandler: SavedStateHandle
) : ViewModel() {
init {
if (!saveHandler.contains(KEY_SUBREDDIT)) {
Log.d(TAG, "set saveHandler")
saveHandler.set(KEY_SUBREDDIT, DEFAULT_SUBREDDIT)
}
}
val posts =
saveHandler.getLiveData<String>(KEY_SUBREDDIT)
.asFlow()
.flatMapLatest {
Log.d(TAG, "saveHandler: $it")
repository.postsOfSubreddit(it, 30)
}
}
SavedStateHandle对象是ViewModel提供的一个以key-value形式的持久化数据的对象,可以通过AbstractSavedStateViewModelFactory的creat()函数传入,这里用于用于存储搜索的关键字。这里获取了key为KEY_SUBREDDIT的搜索关键字的LiveData形式的结果,并且将其转换成流对象,最后将搜索的关键字流对象与上述的flow合成一个新的流对象posts,下面需要观察流对象并将其显示在RecyclerView上面。只要将该流对象赋值给适配器,就可以将数据显示在UI上。Paging专门提供了PagingDataAdapter类来配合 PagingData 使用,下面是在Activity中给Adapte注入数据的代码:
private fun initAdapter() {
val list = findViewById<RecyclerView>(R.id.list)
val pagingAdapter = PagingAdapter()
list.adapter = pagingAdapter
lifecycleScope.launchWhenCreated {
Log.d(TAG, "dataRefreshFlow scope")
@OptIn(androidx.paging.ExperimentalPagingApi::class)
pagingAdapter.dataRefreshFlow.collect {
Log.d(TAG, "dataRefreshFlow collect: $it")
list.scrollToPosition(0)
}
}
lifecycleScope.launchWhenCreated {
Log.d(TAG, "loadStateFlow scope")
pagingAdapter.loadStateFlow.collect {
Log.d(TAG, "loadStateFlow collect: $it")
// swipe_refresh.isRefreshing = it.refresh == LoadState.Loading
}
}
lifecycleScope.launchWhenCreated {
Log.d(TAG, "posts scope")
//观察流对象,并将数据提交给Adapter
mViewModel.posts.collect {
Log.d(TAG, "posts collect: $it")
pagingAdapter.submitData(it)
}
}
}
PagingAdapter的实际类型是PagingDataAdapter,其代码很简单:
class PagingAdapter :
PagingDataAdapter<RedditPost, RedditPostViewHolder>(DIFFER_COMPARATOR) {
override fun onBindViewHolder(holder: RedditPostViewHolder, position: Int) {
holder.bind(getItem(position), holder.itemView.context)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RedditPostViewHolder {
return RedditPostViewHolder.create(parent)
}
companion object {
val DIFFER_COMPARATOR = object : DiffUtil.ItemCallback<RedditPost>() {
override fun areItemsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean {
return oldItem == newItem
}
}
}
}
只需这样简单的几步,即可将网络数据展示在 RecyclerView 上,并且自带预加载功能。