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

简介: Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。

image.png


前言



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


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



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


通过这篇文章你将学习到以下内容:


  • Paging3 是什么?
  • Paging3 相对之前版本 (Paging1、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。
  • 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 时,在应用程序的三层中操作,以及它们如何协同工作加载和显示分页数据,如下图所示:


image.png


我们接下来按照 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 提供了三个方法,如下图所示:


image.png


方法名 作用
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 同时提供了刷新、重试等等方法,如下图所示:


image.png


  • refresh:常用用于下拉更新数据。
  • retry:常用于底部更多样式,当请求网络失败的时候,显示重试按钮,点击调用 retry。


3. Paging3 还帮我处理了如果出现多次网络请求,只会处理最后一次请求,例如由于网络慢,用户频繁的刷新数据等等


6. 监听网路请求状态


刚才分析过 PagingDataAdapter 是一个处理分页数据的可回收视图适配器,并且还提供了两个监听数据状态的方法。


image.png


这两个方法的区别是:


  • 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 都对应着三种状态。


image.png


变量 作用
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,文章都会同步到这个仓库。



工具系列




目录
相关文章
|
3天前
|
存储 监控 安全
云端防御战线:云计算环境下的网络安全策略与实践
【4月更文挑战第23天】在数字化转型的浪潮中,云计算已成为推动企业敏捷性、可扩展性和成本效率的关键因素。然而,随着数据和服务迁移至云端,传统的网络边界逐渐模糊,给网络安全带来了前所未有的挑战。本文探讨了在多租户云环境中维护信息安全的先进策略和技术,分析了云服务模型(IaaS, PaaS, SaaS)特有的安全风险,并提出了一系列针对性的安全措施和最佳实践。通过深入讨论身份与访问管理、数据加密、入侵检测系统以及合规性监控等关键技术,本文旨在为读者提供一套全面的云计算安全防护框架。
5 0
|
6天前
|
安全 数据建模 网络安全
深入理解SSL数字证书:定义、工作原理与网络安全的重要性
本文阐述了SSL数字证书在网络安全中的关键作用,定义了其作为验证服务器身份的数字凭证,基于PKI体系保障数据传输安全。文章介绍了三种类型的证书,包括DV、OV和EV,适用于不同安全需求的网站。获取和安装证书涉及向证书颁发机构申请并部署到服务器。在网络安全挑战下,正确使用和管理SSL证书对于保护用户数据和提升信任度至关重要。
|
7天前
|
人工智能 监控 安全
构筑安全之盾:云计算环境下的网络安全策略与实践
【4月更文挑战第19天】 在数字化转型的浪潮中,云计算已成为企业IT架构的核心组成部分。然而,随着云服务使用的普及化,网络安全问题亦变得日益复杂和挑战性。本文将深入探讨如何在云计算环境中实施有效的网络安全策略,保障数据的安全性和完整性。我们将从云服务模型出发,分析不同服务模型下的安全威胁,并提出相应的防护措施。文章还将讨论信息安全管理的最佳实践,包括加密技术、身份验证、访问控制以及安全监控等方面,旨在为企业提供一套全面的云计算安全防护框架。
|
8天前
|
机器学习/深度学习 数据可视化 测试技术
深度学习:Keras使用神经网络进行简单文本分类分析新闻组数据
深度学习:Keras使用神经网络进行简单文本分类分析新闻组数据
21 0
|
8天前
|
Python 数据可视化 索引
PYTHON用GARCH、离散随机波动率模型DSV模拟估计股票收益时间序列与蒙特卡洛可视化
PYTHON用GARCH、离散随机波动率模型DSV模拟估计股票收益时间序列与蒙特卡洛可视化
24 0
PYTHON用GARCH、离散随机波动率模型DSV模拟估计股票收益时间序列与蒙特卡洛可视化
|
8天前
|
机器学习/深度学习 算法 数据可视化
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析3
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析3
17 0
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析3
|
8天前
|
存储 算法 前端开发
R语言中贝叶斯网络(BN)、动态贝叶斯网络、线性模型分析错颌畸形数据
R语言中贝叶斯网络(BN)、动态贝叶斯网络、线性模型分析错颌畸形数据
30 0
|
9天前
|
网络协议 Java API
深度剖析:Java网络编程中的TCP/IP与HTTP协议实践
【4月更文挑战第17天】Java网络编程重在TCP/IP和HTTP协议的应用。TCP提供可靠数据传输,通过Socket和ServerSocket实现;HTTP用于Web服务,常借助HttpURLConnection或Apache HttpClient。两者结合,构成网络服务基础。Java有多种高级API和框架(如Netty、Spring Boot)简化开发,助力高效、高并发的网络通信。
|
9天前
|
数据可视化 网络可视化
R语言混合图形模型MGM的网络可预测性分析
R语言混合图形模型MGM的网络可预测性分析
|
9天前
|
算法 定位技术 Windows
R语言最大流最小割定理和最短路径算法分析交通网络流量拥堵问题
R语言最大流最小割定理和最短路径算法分析交通网络流量拥堵问题
15 4