Jetpack成员Paging3网络实践及原理分析(二)
前言
之前的文章分析了 App Startup
、 DataStore
、 Paging3
数据库实践。
Jetpack 成员 App Startup 实践及原理分析 再见 SharedPreferences 拥抱 Jetpack DataStore Jetpack成员Paging3 数据库实践及原理分析(一)
今天这篇文章主要来分析 Paging3 网络实践及原理分析,文章中的示例代码,已经上传到 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/Paging3Simple
https://github.com/hi-dhl/AndroidX-Jetpack-Practice
通过这篇文章你将学习到以下内容:
Paging3 是什么? Paging3 相对之前版本 (Paging1、Paging2) 核心的变化? 关于 Paging 支持的分页策略? 在项目中如何使用 Paging3 去加载网络数据? Paging3 网络异常如何处理? Paging3 如何监听网络请求状态? Paging3 如何进行刷新和重试?
Paging3 是什么?
Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。
Google 推荐使用 Paging 作为 App 架构的一部分,它可以很方便的和 Jetpack 组件集成,Paging3 包含了以下功能:
在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。 内置删除重复数据的请求,确保您的 App 有效地使用网络带宽和系统资源。 可配置 RecyclerView 的 adapters,当用户滚动到加载数据的末尾时自动请求数据。 支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava。 内置的错误处理支持,包括刷新和重试等功能。
Paging3 相对于之前类的职能变化
在 Paging3 之前提供了 ItemKeyedDataSource
、PageKeyedDataSource
、 PositionalDataSource
这三个类,在这三个类中进行数据获取的操作。
PositionalDataSource
:主要用于加载数据有限的数据(加载本地数据库)ItemKeyedDataSource
:主要用来请求网络数据,它适用于通过当前页面最后一条数据的 id,作为下一页的数据的开始的位置,例如 Github 的 API。例如地址 https://api.github.com/users?since=0?per_page=30
当 since = 0 时获取第一页数据,当前页面最后一条数据的 ID 是 46。将 46 作为开始位置,此时 since = 46,地址变成: https://api.github.com/users?since=46?per_page=30
。PageKeyedDataSource
:也是用来请求网络数据,它适用于通过页码分页来请求数据。
在 Paging3 之后 ItemKeyedDataSource
、PageKeyedDataSource
、 PositionalDataSource
合并为一个 PagingSource
,所有旧 API 加载方法被合并到 PagingSource
中的单个 load()
方法中。
abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
这是一个挂起函数,实现这个方法来触发异步加载,具体实现见下文,另外在 Paging3 中还有以下变化
LivePagedListBuilder
和RxPagedListBuilder
合并为了Pager
使用 PagedList.Config
替换PagingConfig
使用 RemoteMediator
替换了PagedList.BoundaryCallback
去加载网络和本地数据库的数据
四步实现 Paging3 加载网络数据
Google 推荐我们使用 Paging3 时,在应用程序的三层中操作,以及它们如何协同工作加载和显示分页数据,如下图所示:
我们接下来按照 Google 推荐的方式开始实现,只需要四步即可实现 Paging3 加载网络数据,文中只贴出核心代码,具体实现可以看 GitHub 上的 Paging3SimpleWithNetWork
项目,先在 App 模块中的 build.gradle
文件中添加以下代码:
dependencies {
def paging_version = "3.0.0-alpha01"
implementation "androidx.paging:paging-runtime:$paging_version"
}
1. 网络请求部分
这里选择使用的是 GitHub API
interface GitHubService {
@GET("users")
suspend fun getGithubAccount(@Query("since") id: Int, @Query("per_page") perPage: Int):
List<GithubAccountModel>
companion object {
fun create(): GitHubService {
val client = OkHttpClient.Builder()
.build()
val retrofit = Retrofit.Builder()
.client(client)
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(GitHubService::class.java)
}
}
}
注意: 这里需要在 getGithubAccount
方法前添加 suspend 关键字,否则调用的时候,会抛出以下异常。
Unable to create call adapter for XXXXX
2. 在 Repository 层创建 PagingSource 数据源
class GitHubItemPagingSource(
private val api: GitHubService
) : PagingSource<Int, GithubAccountModel>(), AnkoLogger {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GithubAccountModel> {
return try {
// key 相当于 id
val key = params.key ?: 0
// 获取网络数据
val items = api.getGithubAccount(key, params.loadSize)
// 请求失败或者出现异常,会跳转到 case 语句返回 LoadResult.Error(e)
// 请求成功,构造一个 LoadResult.Page 返回
LoadResult.Page(
data = items, // 返回获取到的数据
prevKey = null, // 上一页,设置为空就没有上一页的效果,这需要注意的是,如果是第一页需要返回 null,否则会出现多次请求
nextKey = items.lastOrNull()?.id// 下一页,设置为空就没有加载更多效果,如果后面没有更多数据设置为空,即滑动到最后不会在加载数据
)
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
}
PagingSource
是一个抽象类,主要用来向 Paging 提供源数据,需要重写 load 方法,在这个方法进行网络请求的处理。需要注意的是LoadResult.Page
里面的两个参数prevKey
和nextKey
,这里有个坑prevKey
:上一页,设置为空就没有上一页的效果,这需要注意的是,如果是第一页需要返回 null,否则会出现多次请求,我刚开始忽略了,导致首次加载的时候,出现了两次请求nextKey
:下一页,设置为空就没有加载更多效果,如果后面没有更多数据设置为空,即滑动到最后不会在加载数据load
方法中的参数LoadParams
,它是一个密封类,里面有三个内部类Refresh
、Append
、Prepend
类名 作用 Refresh 在初始化刷新的使用 Append 在加载更多的时候使用 Prepend 在当前列表头部添加数据的时候使用
3. 在 Repository 层创建 Pager 和 PagingData
Pager
:是主要的入口页面,在其构造方法中接受PagingConfig
、initialKey
、remoteMediator
、pagingSourceFactory
。PagingData
:是分页数据的容器,它查询一个PagingSource
对象并存储结果。
class GitHubRepositoryImpl(
val pageConfig: PagingConfig,
val gitHubApi: GitHubService,
val mapper2Person: Mapper<GithubAccountModel, GitHubAccount>
) : Repository {
override fun postOfData(id: Int): Flow<PagingData<GitHubAccount>> {
return Pager(pageConfig) {
// 加载数据库的数据
GitHubItemPagingSource(gitHubApi, 0)
}.flow.map { pagingData ->
// 数据映射,数据源 GithubAccountModel ——> 上层用到的 GitHubAccount
pagingData.map { mapper2Person.map(it) }
}
}
}
在 postOfData
方法中构建了一个 Pager
, 其构造方法中接受 PagingConfig
、 initialKey
、 remoteMediator
、 pagingSourceFactory
,其中 initialKey
、 remoteMediator
是可选的, pageConfig
和 pagingSourceFactory
必填的。
pagingSourceFactory
是一个 lambda 表达式,在 Kotlin 中可以直接用花括号表示,在花括号内,执行执行网络请求 GitHubItemPagingSource(gitHubApi, 0)
。
最后调用 flow 返回 Flow<PagingData<Value>>
,然后通过 Flow 的 map 方法将数据源 GithubAccountModel
转换成上层用到的 GithubAccount
。
4. 最后一步,接受数据,并绑定 UI
在 ViewModel 接受数据,并传递给 Adapter.
val gitHubLiveData: LiveData<PagingData<GitHubAccount>> =
repository.postOfData(0).asLiveData()
LiveData 有三种使用方式,这里演示的是其中一种,其余的在之前的文章 Jetpack成员Paging3 数据库实践及原理分析(一) 已经分析过了。
mMainViewModel.gitHubLiveData.observe(this, Observer { data ->
mAdapter.submitData(lifecycle, data)
})
到这里请求网络数据并显示的在 UI 上就结束了,最后我们来分析一下 Paging3 内置的错误处理支持,包括刷新和重试等功能。
5. 网络状态异常的处理
Paging3 提供了内置的错误处理支持,包括刷新和重试等功能,说到这里 Google 对于 Paging3 的设计相比于之前的设计真的好,基本上进行网络请求地方用 RecyclerView 去展示数据,都需要用到刷新、重试、错误处理等等功能。
1. 错误处理
Paging3 的组件 PagingDataAdapter
,PagingDataAdapter
是一个处理分页数据的可回收视图适配器, PagingDataAdapter
提供了三个方法,如下图所示:
方法名 | 作用 |
---|---|
withLoadStateFooter | 添加列表底部(类似于加载更多) |
withLoadStateHeader | 添加列表的头部 |
withLoadStateHeaderAndFooter | 添加头部和底部 |
Paging3 提供了 LoadStateAdapter
用于实现列表底部和头部样式,只需要继承 LoadStateAdapter
做对应的网络状态处理即可,例如这里实现的 FooterAdapter
加载更多样式。
class FooterAdapter(val adapter: GitHubAdapter) : LoadStateAdapter<NetworkStateItemViewHolder>() {
override fun onBindViewHolder(holder: NetworkStateItemViewHolder, loadState: LoadState) {
holder.bindData(loadState, 0)
}
override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
): NetworkStateItemViewHolder {
val view = inflateView(parent, R.layout.recycie_item_network_state)
return NetworkStateItemViewHolder(view) { adapter.retry() }
}
private fun inflateView(viewGroup: ViewGroup, @LayoutRes viewType: Int): View {
val layoutInflater = LayoutInflater.from(viewGroup.context)
return layoutInflater.inflate(viewType, viewGroup, false)
}
}
class NetworkStateItemViewHolder(view: View, private val retryCallback: () -> Unit) :
DataBindingViewHolder<LoadState>(view) {
val mBinding: RecycieItemNetworkStateBinding by viewHolderBinding(view)
override fun bindData(data: LoadState, position: Int) {
mBinding.apply {
// 正在加载,显示进度条
progressBar.isVisible = data is LoadState.Loading
// 加载失败,显示并点击重试按钮
retryButton.isVisible = data is LoadState.Error
retryButton.setOnClickListener { retryCallback() }
// 加载失败显示错误原因
errorMsg.isVisible = !(data as? LoadState.Error)?.error?.message.isNullOrBlank()
errorMsg.text = (data as? LoadState.Error)?.error?.message
executePendingBindings()
}
}
}
在上面分别处理了,正在加载、加载失败并提供重试按钮等等状态。
2. Paging3 同时提供了刷新、重试等等方法,如下图所示:
refresh
:常用用于下拉更新数据。retry
:常用于底部更多样式,当请求网络失败的时候,显示重试按钮,点击调用 retry。
3. Paging3 还帮我处理了如果出现多次网络请求,只会处理最后一次请求,例如由于网络慢,用户频繁的刷新数据等等
6. 监听网路请求状态
刚才分析过 PagingDataAdapter
是一个处理分页数据的可回收视图适配器,并且还提供了两个监听数据状态的方法。
这两个方法的区别是:
addDataRefreshListener
:当一个新的PagingData
提交并显示的时候调用。addLoadStateListener
:这个方法同addDataRefreshListener
方法,它们之间的区别是addLoadStateListener
方法返回了一个CombinedLoadStates
的对象,如上图所示。
CombinedLoadStates
是一个数据类,里面有三个成员变量 refresh
、 prepend
和 append
。
val refresh: LoadState = (mediator ?: source).refresh
val prepend: LoadState = (mediator ?: source).prepend
val append: LoadState = (mediator ?: source).append
变量 | 作用 |
---|---|
refresh | 在初始化刷新的使用 |
append | 在加载更多的时候使用 |
prepend | 在当前列表头部添加数据的时候使用 |
refresh
、 prepend
和 append
都是 LoadState
的对象,LoadState
也是一个密封类,每一个 refresh
、prepend
和 append
都对应着三种状态。
变量 | 作用 |
---|---|
Error | 表示加载失败 |
Loading | 表示正在加载 |
NotLoading | 表示当前未加载 |
到这里不得不佩服 Google 什么都替我们想好了,这里需要结合自己的项目实际情况,去定制不同的状态处理。
全文到这里就结束了,本文案例 Paging3SimpleWithNetWork
已经上传到 GitHub,欢迎前去查看。
https://github.com/hi-dhl/AndroidX-Jetpack-Practice
推荐阅读:
最后推荐我一直在更新维护的项目和网站:
最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeLeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析
剑指 offer:https://offer.hi-dhl.com
LeetCode:https://leetcode.hi-dhl.com最新 Android 10 源码分析系列文章
https://github.com/hi-dhl/Android10-Source-Analysis一系列国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
https://github.com/hi-dhl/Technical-Article-Translation「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
https://site.51git.cn
致力于分享一系列最新技术原创文章
长按二维码即可关注
我知道你在看哟