Google 最近更新了几个 Jetpack 新成员 Hilt、Paging 三、App Startup 等等。java
在以前的文章里面分别分析 App Startup 实践以及原理 和 Paging3 加载本地数据(一)实践以及原理,若是没有看过能够点击下方地址前去查看:git
今天这边文章主要来分析 Paging3 加载网络数据及其原理,利用周末的时间参考 Google 文档实现了 Paging3 期间也遇到一些坑,会在文中详细分析,代码已经上传到了 GitHub:Paging3SimpleWithNetWorkgithub
经过这篇文章你将学习到如下内容:面试
在项目 Paging3SimpleWithNetWork 中用到了 Coil(Kotlin 图片加载库)、Databinding(数据绑定)、Anko(主要用来替换替代 XML 使用的方式)、Koin(Kotlin 依赖注入库)、JDatabinding(基于 Databinding 封装的组件)、Data Mapper(数据映射)、使用 Composing builds 做为依赖库的版本管理、Repository 设计模式、MVVM 架构等等,关于这里一些技术以前没有了解过,能够点击下面链接前往查看。算法
Paging 是一个分页库,它能够帮助您从本地存储或经过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。数据库
Google 推荐使用 Paging 做为 App 架构的一部分,它能够很方便的和 Jetpack 组件集成,Paging3 包含了如下功能:编程
在 Paging3 以前提供了 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 这三个类,在这三个类中进行数据获取的操做。设计模式
https://api.github.com/users?since=0?per_page=30
当 since = 0 时获取第一页数据,当前页面最后一条数据的 ID 是 46。https://api.github.com/users?since=46?per_page=30
。在 Paging3 以后 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 合并为一个 PagingSource,全部旧 API 加载方法被合并到 PagingSource 中的单个 load() 方法中。api
abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
复制代码
这是一个挂起函数,实现这个方法来触发异步加载,具体实现见下文,另外在 Paging3 中还有如下变化数组
Google 推荐咱们使用 Paging3 时,在应用程序的三层中操做,以及它们如何协同工做加载和显示分页数据,以下图所示:
咱们接下来按照 Google 推荐的方式开始实现,只须要四步便可实现 Paging3 加载网络数据,文中只贴出核心代码,具体实现能够看 GitHub 上的 Paging3SimpleWithNetWork 项目。
这里选择使用的是 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
复制代码
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,这里有个坑。
load 方法的参数 LoadParams,它是一个密封类,里面有三个内部类 Refresh、Append、Prepend。
类名 | 做用 |
---|---|
Refresh | 在初始化刷新的使用 |
Append | 在加载更多的时候使用 |
Prepend | 在当前列表头部添加数据的时候使用 |
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。
关于 flow 在上一篇 Jetpack 成员 Paging3 实践以及源码分析(一) 已经分析过了.
在 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 内置的错误处理支持,包括刷新和重试等功能。
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 同时提供了刷新、重试等等方法,以下图所示:
3. Paging3 还帮我处理了若是出现屡次网络请求,只会处理最后一次请求,例如因为网络慢,用户频繁的刷新数据等等
刚才分析过 PagingDataAdapter 是一个处理分页数据的可回收视图适配器,而且还提供了两个监听数据状态的方法。
这两个方法的区别是:
CombinedLoadStates 是一个数据类,里面有三个成员变量 refresh、prepend 和 append。
val refresh: LoadState = (mediator ?: source).refresh
val prepend: LoadState = (mediator ?: source).prepend
val append: LoadState = (mediator ?: source).append
复制代码
变量 | 做用 |
---|---|
refresh | 在初始化刷新的使用 |
prepend | 在加载更多的时候使用 |
append | 在当前列表头部添加数据的时候使用 |
refresh、prepend 和 append 都是 LoadState 的对象,LoadState 也是一个密封类,每个 refresh、prepend 和 append 都对应着三种状态。
变量 | 做用 |
---|---|
Error | 表示加载失败 |
Loading | 表示正在加载 |
NotLoading | 表示当前未加载 |
到这里不得不佩服 Google 什么都替咱们想好了,这里须要结合本身的项目实际状况,去定制不一样的状态处理。
到这里 Paging3 算是完结了,最后贴一下本文案例 Paging3SimpleWithNetWork 已经上传到 GitHub,最后祝你们周末愉快呀。
计划创建一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增长 Jetpack 新成员,仓库持续更新,能够前去查看:AndroidX-Jetpack-Practice, 若是这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。
致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,若是这篇文章对你有帮助给个 star,一块儿来学习,期待与你一块儿成长。
因为 LeetCode 的题库庞大,每一个分类都能筛选出数百道题,因为每一个人的精力有限,不可能刷完全部题目,所以我按照经典类型题目去分类、和题目的难易程度去排序。
每道题目都会用 Java 和 kotlin 去实现,而且每道题目都有解题思路,若是你同我同样喜欢算法、LeetCode,能够关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一块儿来学习,期待与你一块儿成长。
正在写一系列的 Android 10 源码分析的文章,了解系统源码,不只有助于分析问题,在面试过程当中,对咱们也是很是有帮助的,若是你同我同样喜欢研究 Android 源码,能够关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。
目前正在整理和翻译一系列精选国外的技术文章,不只仅是翻译,不少优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深刻的解读,能够关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。