查看原文
其他

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

hi-dhl ByteCode 2021-10-13

前言

之前的文章分析了 App StartupDataStorePaging3  数据库实践。

今天这篇文章主要来分析 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 之前提供了 ItemKeyedDataSourcePageKeyedDataSourcePositionalDataSource 这三个类,在这三个类中进行数据获取的操作。

  • 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 之后 ItemKeyedDataSourcePageKeyedDataSourcePositionalDataSource 合并为一个 PagingSource ,所有旧 API 加载方法被合并到 PagingSource 中的单个 load() 方法中。

abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>

这是一个挂起函数,实现这个方法来触发异步加载,具体实现见下文,另外在 Paging3 中还有以下变化

  • LivePagedListBuilderRxPagedListBuilder 合并为了 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 里面的两个参数 prevKeynextKey这里有个坑

    • prevKey :上一页,设置为空就没有上一页的效果,这需要注意的是,如果是第一页需要返回 null,否则会出现多次请求,我刚开始忽略了,导致首次加载的时候,出现了两次请求
    • nextKey :下一页,设置为空就没有加载更多效果,如果后面没有更多数据设置为空,即滑动到最后不会在加载数据
  • load 方法中的参数 LoadParams,它是一个密封类,里面有三个内部类 RefreshAppendPrepend

    类名作用
    Refresh在初始化刷新的使用
    Append在加载更多的时候使用
    Prepend在当前列表头部添加数据的时候使用

3. 在 Repository 层创建 Pager 和 PagingData

  • Pager:是主要的入口页面,在其构造方法中接受 PagingConfiginitialKeyremoteMediatorpagingSourceFactory
  • 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, 其构造方法中接受 PagingConfiginitialKeyremoteMediatorpagingSourceFactory ,其中 initialKeyremoteMediator 是可选的, pageConfigpagingSourceFactory 必填的。

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 的组件 PagingDataAdapterPagingDataAdapter 是一个处理分页数据的可回收视图适配器, 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 是一个数据类,里面有三个成员变量 refreshprependappend

val refresh: LoadState = (mediator ?: source).refresh
val prepend: LoadState = (mediator ?: source).prepend
val append: LoadState = (mediator ?: source).append
变量作用
refresh在初始化刷新的使用
append在加载更多的时候使用
prepend在当前列表头部添加数据的时候使用

refreshprependappend 都是 LoadState 的对象,LoadState 也是一个密封类,每一个 refreshprependappend 都对应着三种状态。

变量作用
Error表示加载失败
Loading表示正在加载
NotLoading表示当前未加载

到这里不得不佩服 Google 什么都替我们想好了,这里需要结合自己的项目实际情况,去定制不同的状态处理。

全文到这里就结束了,本文案例 Paging3SimpleWithNetWork 已经上传到 GitHub,欢迎前去查看。

https://github.com/hi-dhl/AndroidX-Jetpack-Practice


推荐阅读:



最后推荐我一直在更新维护的项目和网站:

  • 最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
    https://github.com/hi-dhl/AndroidX-Jetpack-Practice

  • LeetCode / 剑指 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



致力于分享一系列最新技术原创文章

长按二维码即可关注


我知道你在看

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存