Jetpack 新成员 Paging3 网络实践及原理分析(二)

前言

Google 最近更新了几个 Jetpack 新成员 Hilt、Paging 三、App Startup 等等。java

在以前的文章里面分别分析 App Startup 实践以及原理Paging3 加载本地数据(一)实践以及原理,若是没有看过能够点击下方地址前去查看:git

今天这边文章主要来分析 Paging3 加载网络数据及其原理,利用周末的时间参考 Google 文档实现了 Paging3 期间也遇到一些坑,会在文中详细分析,代码已经上传到了 GitHub:Paging3SimpleWithNetWorkgithub

经过这篇文章你将学习到如下内容:面试

  • Paging3 是什么?
  • Paging3 相对以前版本 (Paging一、Paging2) 核心的变化?
  • 关于 Paging 支持的分页策略?
  • 在项目中如何使用 Paging3 去加载网络数据?
  • Paging3 网络异常如何处理?
  • Paging3 如何监听网络请求状态?
  • Paging3 如何进行刷新和重试?

在项目 Paging3SimpleWithNetWork 中用到了 Coil(Kotlin 图片加载库)、Databinding(数据绑定)、Anko(主要用来替换替代 XML 使用的方式)、Koin(Kotlin 依赖注入库)、JDatabinding(基于 Databinding 封装的组件)、Data Mapper(数据映射)、使用 Composing builds 做为依赖库的版本管理、Repository 设计模式、MVVM 架构等等,关于这里一些技术以前没有了解过,能够点击下面链接前往查看。算法

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() 方法中。api

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 项目。

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。

关于 flow 在上一篇 Jetpack 成员 Paging3 实践以及源码分析(一) 已经分析过了.

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 在初始化刷新的使用
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 10 源码分析的文章,了解系统源码,不只有助于分析问题,在面试过程当中,对咱们也是很是有帮助的,若是你同我同样喜欢研究 Android 源码,能够关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

Android 应用系列

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不只仅是翻译,不少优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深刻的解读,能够关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

工具系列

相关文章
相关标签/搜索