学校网站开发工程师,网站优化人员,项目设计高中综评,最大的免费网站建设作者#xff1a;却把清梅嗅链接#xff1a;https://github.com/qingmei2/blogs/issues/30前言本文将对Paging分页组件的设计和实现进行一个系统整体的概述#xff0c;强烈建议 读者将本文作为学习Paging 阅读优先级最高的文章#xff0c;所有其它的Paging中文博客阅读优先级… 作者却把清梅嗅链接https://github.com/qingmei2/blogs/issues/30前言本文将对Paging分页组件的设计和实现进行一个系统整体的概述强烈建议 读者将本文作为学习Paging 阅读优先级最高的文章所有其它的Paging中文博客阅读优先级都应该靠后。本文篇幅 较长整体结构思维导图如下一、起源手机应用中列表是常见的界面构成元素而对于Android开发者而言RecyclerView是实现列表的不二选择。在正式讨论Paging和列表分页功能之前我们首先看看对于一个普通的列表开发者如何通过代码对其进行建模如图所示针对这样一个简单 联系人界面 的建模我们引出3个重要的层级1.服务端组件、数据库、内存为什么说 服务端组件、数据库 以及 内存 是非常重要的三个层级呢首先开发者为当前页面创建了一个ViewModel并通过成员变量在 内存 中持有了一组联系人数据因为ViewModel组件的原因即使页面配置发生了改变(比如屏幕的旋转)数据依然会被保留下来。而 数据库 的作用则保证了App即使在离线环境下用户依然可以看到一定的内容——显然对于上图中的页面(联系人列表)而言本地缓存是非常有意义的。对于绝大多数列表而言服务端 往往意味着是数据源每当用户执行刷新操作App都应当尝试向服务端请求最新的数据并将最新的数据存入 数据库并随之展示在UI上。通常情况下这三个层级并非同时都是必要的读者需正确理解三者各自不同的使用场景。现在借助于 服务端组件、数据库 以及 内存开发者将数据展示在RecyclerView上这似乎已经是正解了。2.问题在哪到目前为止问题还没有完全暴露出来。我们忽视了一个非常现实的问题那就是 数据是动态的 ——这意味着每当数据发生了更新(比如用户进行了下拉刷新操作)开发者都需要将最新的数据响应在UI上。这意味着当某个用户的联系人列表中有10000个条目时每次数据的更新都会对所有的数据进行重建——从而导致 性能非常低下用户看到的只是屏幕中的几条联系人信息为此要重新创建10000个条目用户显然无法接受。因此分页组件的设计势在必行。3.整理需求3.1、简单易用上文我们谈到UI响应数据的变更这种情况下使用 观察者模式 是一个不错的主意比如LiveData、RxJava甚至自定义一个接口等等开发者仅需要观察每次数据库中数据的变更并进行UI的更新class MyViewModel : ViewModel() { val users: LiveData}新的组件我们也希望能拥有同样的便利比如使用LiveData或者RxJava并进行订阅处理数据的更新—— 简单 且 易用。3.2、处理更多层级我们希望新的组件能够处理多层我们希望列表展示 服务器 返回的数据、 或者 数据库 中的数据并将其放入UI中。3.3、性能新的组件必须保证足够的快不做任何没必要的行为为了保证效率繁重的操作不要直接放在UI线程中处理。3.4、感知生命周期 如果可能新的组件需要能够对生命周期进行感知就像LiveData一样如果页面并不在屏幕的可视范围内组件不应该工作。3.5、足够灵活足够的灵活性非常重要——每个项目都有不同的业务这意味着不同的API、不同的数据结构新的组件必须保证能够应对所有的业务场景。这一点并非必须但是对于设计者来说难度不小这意味着需要将不同的业务中的共同点抽象出来并保证这些设计适用在任何场景中。定义好了需求在正式开始设计Paging之前首先我们先来回顾一下普通的列表如何实现数据的动态更新的。4.普通列表的实现方式我们依然通过 联系人列表 作为示例来描述普通列表如何响应数据的动态更新。首先我们需要定义一个Dao这里我们使用了Room组件用于 数据库 中联系人的查询Daointerface UserDao {Query(SELECT * FROM user)fun queryUsers(): LiveData}这里我们返回的是一个LiveData正如我们前文所言构建一个可观察的对象显然会让数据的处理更加容易。接下来我们定义好ViewModel和Activity:class MyViewModel(val dao: UserDao) : ViewModel() {// 1.定义好可观察的LiveDataval users: LiveData dao.queryUsers()}class MyActivity : Activity {val myViewModel: MyViewModelval adapter: ListAdapterfun onCreate(bundle: Bundle?) {// 2.在Activity中对LiveData进行订阅 myViewModel.users.observe(this) {// 3.每当数据更新计算新旧数据集的差异对列表进行更新 adapter.submitList(it) } }}这里我们使用到了ListAdapter它是官方基于RecyclerView.Adapter的AsyncListDiffer封装类其内创建了AsyncListDiffer的示例以便在后台线程中使用DiffUtil计算新旧数据集的差异从而节省Item更新的性能。本文默认读者对ListAdapter一定了解如果不是很熟悉请参考DiffUtil、AsyncListDiffer、ListAdapter等相关知识点的文章。此外我们还需要在ListAdapter中声明DiffUtil.ItemCallback对数据集的差异计算的逻辑进行补充class MyAdapter(): ListAdapterUser, UserViewHolder(object: DiffUtil.ItemCallbackUser() {override fun areItemsTheSame(oldItem: User, newItem: User) oldItem.id newItem.idoverride fun areContentsTheSame(oldItem: User, newItem: User) oldItem newItem }) {// ...}Thats all, 接下来我们开始思考新的分页组件应该是什么样的。二、分页组件简介1.核心类PagedList上文提到一个普通的RecyclerView展示的是一个列表的数据比如List但在列表分页的需求中List明显就不太够用了。为此Google设计出了一个新的角色PagedList顾名思义该角色的意义就是 分页列表数据的容器 。既然有了List为什么需要额外设计这样一个PagedList的数据结构本质原因在于加载分页数据的操作是异步的 因此定义PagedList的第二个作用是 对分页数据的异步加载 ,这个我们后文再提。现在我们的ViewModel现在可以定义成这样因为PagedList也作为列表数据的容器(就像List一样)class MyViewModel : ViewModel() {// before// val users: LiveData dao.queryUsers()// afterval users: LiveData dao.queryUsers()}在ViewModel中开发者可以轻易通过对users进行订阅以响应分页数据的更新这个LiveData的可观察者是通过Room组件创建的我们来看一下我们的dao:Daointerface UserDao {// 注意这里 LiveData 改成了 LiveDataQuery(SELECT * FROM user)fun queryUsers(): LiveData}乍得一看似乎理所当然但实际需求中有一个问题这里的定义是模糊不清的——对于分页数据而言不同的业务场景所需要的相关配置是不同的。那么什么是分页相关配置呢最直接的一点是每页数据的加载数量PageSize不同的项目都会自行规定每页数据量的大小一页请求15个数据还是20个数据显然我们目前的代码无法进行配置这是不合理的。2.数据源: DataSource及其工厂回答这个问题之前我们还需要定义一个角色用来为PagedList容器提供分页数据那就是数据源DataSource。什么是DataSource呢它不应该是 数据库数据 或者 服务端数据 而应该是 数据库数据 或者 服务端数据 的一个快照(Snapshot)。每当Paging被告知需要更多数据“Hi我需要第45-60个的数据”——数据源DataSource就会将当前Snapshot对应索引的数据交给PagedList。但是我们需要构建一个新的PagedList的时候——比如数据已经失效DataSource中旧的数据没有意义了因此DataSource也需要被重置。在代码中这意味着新的DataSource对象被创建因此我们需要提供的不是DataSource而是提供DataSource的工厂。为什么要提供DataSource.Factory而不是一个DataSource? 复用这个DataSource不可以吗当然可以但是将DataSource设置为immutable(不可变)会避免更多的未知因素。重新整理思路我们如何定义Dao中接口的返回值呢Daointerface UserDao {// Int 代表按照数据的位置(position)获取数据// User 代表数据的类型Query(SELECT * FROM user)fun queryUsers(): DataSource.FactoryInt, User}返回的是一个数据源的提供者DataSource.Factory页面初始化时会通过工厂方法创建一个新的DataSource这之后对应会创建一个新的PagedList每当PagedList想要获取下一页的数据数据源都会根据请求索引进行数据的提供。当数据失效时DataSource.Factory会再次创建一个新的DataSource其内部包含了最新的数据快照(本案例中代表着数据库中的最新数据)随后创建一个新的PagedList并从DataSource中取最新的数据进行展示——当然这之后的分页流程都是相同的无需再次复述。笔者绘制了一幅图用于描述三者之间的关系读者可参考上述文字和图片加以理解3.串联两者PagedListBuilder回归第一小节的那个问题分页相关业务如何进行配置我们虽然介绍了为PagedList提供数据的DataSource但这个问题似乎还是没有得到解决。此外现在Dao中接口的返回值已经是DataSource.Factory而ViewModel中的成员被观察者则是LiveData类型如何 将数据源的工厂和LiveData进行串联 因此我们还需要定义一个新的角色PagedListBuilder开发者将 数据源工厂 和 相关配置 统一交给PagedListBuilder即可生成对应的LiveData:class MyViewModel(val dao: UserDao) : ViewModel() {val users: LiveDatainit {// 1.创建DataSource.Factoryval factory: DataSource.Factory dao.queryUsers()// 2.通过LivePagedListBuilder配置工厂和pageSize, 对users进行实例化 users LivePagedListBuilder(factory, 30).build() }}如代码所示我们在ViewModel中先通过dao获取了DataSource.Factory工厂创建数据源DataSource后者为PagedList提供列表所需要的数据此外另外一个Int类型的参数则制定了每页数据加载的数量这里我们指定每页数据数量为30。我们成功创建了一个LiveData的可观察者对象接下来的步骤读者驾轻就熟只不过我们这里使用的是PagedListAdapterclass MyActivity : Activity {val myViewModel: MyViewModel// 1.这里我们使用PagedListAdapterval adapter: PagedListAdapterfun onCreate(bundle: Bundle?) {// 2.在Activity中对LiveData进行订阅 myViewModel.users.observe(this) {// 3.每当数据更新计算新旧数据集的差异对列表进行更新 adapter.submitList(it) } }}PagedListAdapter内部的实现和普通列表ListAdapter的代码几乎完全相同// 几乎完全相同的代码只有继承的父类不同class MyAdapter(): PagedListAdapterUser, UserViewHolder(object: DiffUtil.ItemCallbackUser() {override fun areItemsTheSame(oldItem: User, newItem: User) oldItem.id newItem.idoverride fun areContentsTheSame(oldItem: User, newItem: User) oldItem newItem }) {// ...}准确的来说两者内部的实现还有微弱的区别前者ListAdapter的getItem()函数的返回值是User,而后者PagedListAdapter返回值应该是User?(Nullable),其原因我们会在下面的Placeholder部分进行描述。4.更多可选配置PagedList.Config目前的介绍中分页的功能似乎已经实现完毕但这些在现实开发中往往不够产品业务还有更多细节性的需求。在上一小节中我们通过LivePagedListBuilder对LiveData进行创建这其中第二个参数是 分页组件的配置代表了每页加载的数量(PageSize) // beforeval users: LiveData LivePagedListBuilder(factory, 30).build()读者应该理解分页组件的配置 本身就是抽象的PageSize并不能完全代表它因此设计者额外定义了更复杂的数据结构PagedList.Config以描述更细节化的配置参数// afterval config PagedList.Config.Builder() .setPageSize(15) // 分页加载的数量 .setInitialLoadSizeHint(30) // 初次加载的数量 .setPrefetchDistance(10) // 预取数据的距离 .setEnablePlaceholders(false) // 是否启用占位符 .build()// API发生了改变val users: LiveDataPagedListUser LivePagedListBuilder(factory, config).build()对复杂业务配置的API设计来说建造者模式 显然是不错的选择。接下来我们简单了解一下这些可选的配置分别代表了什么。4.1.分页数量PageSize最易理解的配置分页请求数据时开发者总是需要定义每页加载数据的数量。4.2.初始加载数量InitialLoadSizeHint定义首次加载时要加载的Item数量。此值通常大于PageSize因此在初始化列表时该配置可以使得加载的数据保证屏幕可以小范围的滚动。如果未设置则默认为PageSize的三倍。4.3.预取距离PrefetchDistance顾名思义该参数配置定义了列表当距离加载边缘多远时进行分页的请求默认大小为PageSize——即距离底部还有一页数据时开启下一页的数据加载。若该参数配置为0则表示除非明确要求否则不会加载任何数据通常不建议这样做因为这将导致用户在滚动屏幕时看到占位符或列表的末尾。4.4.是否启用占位符PlaceholderEnabled该配置项需要传入一个boolean值以决定列表是否开启placeholder(占位符)那么什么是placeholder呢我们先来看未开启占位符的情况如图所示没有开启占位符的情况下列表展示的是当前所有的数据请读者重点观察图片右侧的滚动条当滚动到列表底部成功加载下一页数据后滚动条会从长变短这意味着新的条目成功实装到了列表中。一言以蔽之未开启占位符的列表条目的数量和PagedList中数据数量是一致的。接下来我们看一下开启了占位符的情况如图所示开启了占位符的列表条目的数量和DataSource中数据的总量是一致的。这并不意味着列表从DataSource一次加载了大量的数据并进行渲染所有业务依然交给Paging进行分页处理。当用户滑动到了底部尚未加载的数据时开发者会看到还未渲染的条目这是理所当然的PagedList的分页数据加载是异步的这时对于Item的来说要渲染的数据为null因此开发者需要配置占位符当数据未加载完毕时UI如何进行渲染——这也正是为何上文说到对于PagedListAdapter来说getItem()函数的返回值是可空的User?而不是User。随着PagedList下一页数据的异步加载完毕伴随着RecyclerView的原生动画新的数据会被重新覆盖渲染到placeholder对应的条目上就像gif图展示的一样。4.5.关于Placeholder这里我专门开一个小节谈谈关于placeholder因为这个机制和我们传统的分页业务似乎有所不同但Google的工程师们认为在某些业务场景下该配置确实很有用。开启了占位符用户总是可以快速的滑动列表因为列表“持有”了整个数据集因此不会像未开启占位符时滑动到底部而被迫暂停滚动直到新的数据的加载完毕才能继续浏览。顺畅的操作总比期望之外的阻碍要好得多 。此外开启了占位符意味着用户与 加载指示器 彻底告别类似一个 正在加载更多... 的提示标语或者一个简陋的ProgressBar效果真的会提升用户体验吗也许答案是否定的相比之下用户应该更喜欢一个灰色的占位符并等待它被新的数据渲染。但缺点也随之而来首先占位符的条目高度应该和正确的条目高度一致在某些需求中这也许并不符合这将导致渐进性的动画效果并不会那么好。其次对于开发者而言开启占位符意味着需要对ViewHolder进行额外的代码处理数据为null或者不为null两种情况下的条目渲染逻辑都需要被添加。最后这是一个限制性的条件您的DataSource数据源内部的数据数量必须是确定的比如通过Room从本地获取联系人列表而当数据通过网络请求获取的话这时数据的数量是不确定的不开启Placeholder反而更好。5.更多观察者类型的配置在本文的示例中我们建立了一个LiveData的可观察者对象供用户响应数据的更新实际上组件的设计应该面向提供对更多优秀异步库的支持比如RxJava。因此和LivePagedListBuilder一样设计者还提供了RxPagedListBuilder通过DataSource数据源和PagedList.Config以构建一个对应的Observable:// LiveData supportval users: LiveData LivePagedListBuilder(factory, config).build()// RxJava supportval users: Observable RxPagedListBuilder(factory, config).buildObservable()三、工作流程原理概述Paging幕后是如何工作的接下来笔者将针对Paging分页组件的工作流程进行系统性的描述探讨Paging是 如何实现异步分页数据的加载和响应 的。为了便于理解笔者将整个流程拆分为三个步骤并为每个步骤绘制对应的一张流程图这三个步骤分别是1、初次创建流程2、UI渲染和分页加载流程3、刷新数据源流程1、初次创建流程如图所示我们定义了ViewModel和RepositoryRepository内部实现了App的数据加载的逻辑而其左侧的ViewModel则负责与UI组件的通信。Repository负责为ViewModel中的LiveData进行创建因此开发者需要创建对应的PagedList.Config分页配置对象和DataSource.Factory数据源的工厂并通过调用LivePagedListBuilder相关的API创建出一个LiveData。当LiveData一旦被订阅Paging将会尝试创建一个PagedList同时数据源的工厂DataSource.Factory也会创建一个DataSource并交给PagedList持有该DataSource。这时候PagedList已经被成功的创建了但是此时的PagedList内部只持有了一个DataSource却并没有持有任何数据这意味着观察者角色的UI层即将接收到一个空数据的PagedList。这没有任何意义因此我们更希望PagedList第一次传递到UI层级的同时已经持有了初始的列表数据(即InitialLoadSizeHint)因此Paging尝试在后台线程中通过DataSource对PagedList内部的数据列表进行初始化。现在PagedList第一次创建完毕并持有属于自己的DataSource和初始的列表数据通过LiveData这个管道即将向UI层迈出属于自己的第一个脚印。2.UI渲染和分页加载流程通过内部线程的切换PagedList从后台线程切换到了UI线程通过LiveData抵达了UI层级也就是我们通常说的Activity或者Fragment中。读者应该有印象在上文的示例代码中Activity观察到PagedList后会通过PagedListAdapter.submitList()函数将PagedList进行注入。PagedListAdapter第一次接收到PagedList后就会对UI进行渲染。当用户尝试对屏幕中的列表进行滚动时我们接收到了需要加载更多数据的信号这时PagedList在内部主动触发数据的加载数据源提供了更多的数据PagedList接收到之后将会主动触发RecyclerView的更新用户通过RecyclerView原生动画观察到了更多的列表Item。3.刷新数据源流程当数据发生了更新Paging幕后又做了哪些工作呢正如前文所说数据是动态的 假设用户通过操作添加了一个联系人这时数据库中的数据集发生了更新。因此这时屏幕中RecyclerView对应的PagedList和DataSource已经没有失效了因为DataSource中的数据是之前数据库中数据的快照数据库内部进行了更新PagedList从旧的DataSource中再取数据毫无意义。因此Paging组件接收到了数据失效的信号这意味着生产者需要重新构建一个PagedList因此DataSource.Factory再次提供新版本的数据源DataSource V2——其内部持有了最新数据的快照。在创建新的PagedList的时候针对PagedList内部的初始化需要慎重考虑因为初始化的数据需要根据用户当前屏幕中所在的位置(position)进行加载。通过LiveDataUI层级再次观察到了新的PagedList并再次通过submitList()函数注入到PagedListAdapter中。和初次的数据渲染不同这一次我们使用到了PagedListAdapter内部的AsyncPagedListDiffer对两个数据集进行差异性计算——这避免了notifyDataSetChanged()的滥用同时差异性计算的任务被切换到了后台线程中执行一旦计算出差异性结果新的PagedList会替换旧的PagedList并对列表进行 增量更新。四、DataSource数据源简介Paging分页组件的设计中DataSource是一个非常重要的模块。顾名思义DataSource中的Key对应数据加载的条件Value对应数据集的实际类型 针对不同场景Paging的设计者提供了三种不同类型的DataSource抽象类:PositionalDataSourceTItemKeyedDataSourceKey, ValuePageKeyedDataSourceKey, Value接下来我们分别对其进行简单的介绍。本章节涉及的知识点非常重要但不作为本文的重点笔者将在该系列的下一篇文章中针对DataSource的设计与实现进行更细节的探究欢迎关注。1.PositionalDataSourcePositionalDataSource 是最简单的DataSource类型顾名思义其通过数据所处当前数据集快照的位置(position)提供数据。PositionalDataSource 适用于 目标数据总数固定通过特定的位置加载数据这里Key是Integer类型的位置信息并且被内置固定在了PositionalDataSource类中T即数据的类型。最容易理解的例子就是本文的联系人列表其所有的数据都来自本地的数据库这意味着数据的总数是固定的我们总是可以根据当前条目的position映射到DataSource中对应的一个数据。来看Room组件配置的dao对应编译期生成的源码java// 1.Room自动生成了 DataSource.FactoryOverridepublic DataSource.FactorygetAllStudent() {// 2.工厂函数提供了PositionalDataSourcereturn new DataSource.Factory() {Overridepublic PositionalDataSourcecreate() {return new PositionalDataSource(__db, _statement, false , Student) {// ... }; } }; }2.ItemKeyedDataSourceItemKeyedDataSource适用于目标数据的加载依赖特定条目的信息比如需要根据第N项的信息加载第N1项的数据传参中需要传入第N项的某些信息时。同样拿联系人列表举例另外的一种分页加载方式是通过上一个联系人的name作为Key请求新一页的数据因为联系人name字母排序的原因DataSource很容易针对一个name检索并提供接下来新一页的联系人数据——比如根据Alice找到下一个用户Bob(A - B)。3.PageKeyedDataSource更多的网络请求API中服务器返回的数据中都会包含一个String类型类似nextPage的字段以表示当前页数据的下一页数据的接口(比如Github的API)这种分页数据加载的方式正是PageKeyedDataSource的拿手好戏。这是日常开发中用到最多的DataSource类型和ItemKeyedDataSource不同的是前者的数据检索关系是单个数据与单个数据之间的后者则是每一页数据和每一页数据之间的。同样拿联系人列表举例这种分页加载方式是按照页码进行数据加载的比如一次请求15条数据服务器返回数据列表的同时会返回下一页数据的url(或者页码)借助该参数请求下一页数据成功后服务器又回返回下下一页的url以此类推。总的来说DataSource针对不同种数据分页的加载策略提供了不同种的抽象类以方便开发者调用很多情况下同样的业务使用不同的DataSource都能够实现开发者按需取用即可。五、最佳实践现在读者对多种不同的数据源DataSource有了简单的了解先抛开 分页列表 的业务不谈我们思考另外一个问题当列表的数据通过多个层级 网络请求(Network) 和 本地缓存 (Database)进行加载该怎么处理回答这个问题需要先思考另外一个问题NetworkDatabase的解决方案有哪些优势1.优势读者认真思考可得NetworkDatabase的解决方案优点如下1、非常优秀的离线模式支持即使用户设备并没有链接网络本地缓存依然可以带来非常不错的使用体验2、数据的快速恢复如果异常导致App的终止本地缓存可以对页面数据进行快速恢复大幅减少流量的损失以及加载的时间。3、两者的配合的效果总是相得益彰。看起来NetworkDatabase是一个非常不错的数据加载方案那么为什么大多数场景并没有使用本地缓存呢主要原因是开发成本——本地缓存的搭建总是需要额外的代码不仅如此更重要的原因是数据交互的复杂性也会导致额外的开发成本。2.复杂的交互模型为什么说NetworkDatabase会导致 数据交互的复杂性 让我们回到本文的 联系人列表 的示例中这个示例中所有联系人数据都来自 本地缓存因此读者可以很轻易的构建出该功能的整体结构如图所示ViewModel中的数据总是由Database提供如果把数据源从Database换成Network数据交互的模型也并没有什么区别—— 数据源总是单一的。那么当数据的来源不唯一时——即NetworkDatabase的数据加载方案中会有哪些问题呢我们来看看常规的实现方案的数据模型如图所示ViewModel尝试加载数据时总是会先进行网络判断若网络未连接则展示本地缓存否则请求网络并且在网络请求成功时将数据保存本地。乍得一看这种方案似乎并没有什么问题实际上却有两个非常大的弊端2.1 业务并非这么简单首先通过一个boolean类型的值就能代表网络连接的状态吗显而易见答案是否定的。实际上在某些业务场景下服务器的连接状态可以是更为复杂的比如接收到了部分的数据包比如某些情况下网络请求错误这时候是否需要重新展示本地缓存若涉及到网络请求的重试则更复杂成功展示网络数据再次失败展示缓存——业务越来越复杂我们甚至会逐渐沉浸其中无法自拔最终醒悟这种数据的交互模型完全不够用了 。2.2 无用的本地缓存另外一个很明显的弊端则是当网络连接状态良好的时候用户看到的数据总是服务器返回的数据。这种情况下请求的数据再次存入本地缓存似乎毫无意义因为网络环境的通畅Database中的缓存从来未作为数据源被展示过。3.使用单一数据源使用 单一数据源 (single source of truth)的好处不言而喻正如上文所阐述的多个数据源 反而会将业务逻辑变得越来越复杂因此我们设计出这样的模型ViewModel如果响应Database中的数据变更且Database作为唯一的数据来源其思路是ViewModel只从Database中取得数据当Database中数据不够时则向Server请求网络数据请求成功数据存入DatabaseViewModel观察到Database中数据的变更并更新到UI中。这似乎无法满足上文中的需求读者认真思考可知其实是没问题的当网络连接发生故障时这时向服务端请求数据失败并不会更新Database因此UI展示的正是期望的本地缓存。ViewModel仅仅响应Database中数据的变更这种使用 单一数据源 的方式让复杂的业务逻辑简化了很多。4.分页列表的最佳实践现在我们理解了 单一数据源 的好处该方案在分页组件中也同样适用我们唯一需要实现的是如何主动触发服务端数据的请求这是当然的因为Database中依赖网络请求成功之后的数据存储更新否则列表所展示的永远是Database中不变的数据——别忘了ViewModel和Server之间并没有任何关系。针对Database中的数据更新简单的方式是 直接进行网络请求这种方式使用非常普遍比如列表需要下拉刷新这时主动请求网络网络请求成功后将数据存入数据库即可这时ViewModel响应到数据库中的更新并将最新的数据更新在UI上。另外一种方式则和Paging分页组件本身有关当列表滚动到指定位置需要对下一页数据进行加载时如何向网络拉取最新数据Paging为此提供了BoundaryCallback类用于配置分页列表自动请求分页数据的回调函数其作用是当数据库中最后一项数据被加载时则会调用其onItemAtEndLoaded函数class MyBoundaryCallback(val database : MyLocalCacheval apiService: ApiService) : PagedList.BoundaryCallbackUser() {override fun onItemAtEndLoaded(itemAtEnd: User) {// 请求网络数据并更新到数据库中 requestAndAppendData(apiService, database, itemAtEnd) }}BoundaryCallback类为Paging通过NetworkDatabase进行分页加载的功能完成了最后一块拼图现在分页列表所有数据都来源于本地缓存并且复杂的业务实现起来也足够灵活。5.更多优势通过NetworkDatabase进行Paging分页加载还有更多好处比如更轻易管理分页列表 额外的状态 。不仅仅是分页列表这种方案使得所有列表的 状态管理 的更加容易笔者为此撰写了另外一篇文章去阐述它篇幅所限本文不进行展开有兴趣的读者可以阅读。Android官方架构组件Paging-Ex:列表状态的响应式管理https://juejin.im/post/5ce6ba09e51d4555e372a562六、总结本文对Paging进行了系统性的概述最后Paging到底是一个什么样的分页库首先它支持Network、Database或者两者通过Paging你可以轻松获取分页数据并直接更新在RecyclerView中。其次Paging使用了非常优秀的 观察者模式 其简单的API的内部封装了复杂的分页逻辑。第三Paging灵活的配置和强大的支持——不同DataSource的数据加载方式、不同的响应式库的支持(LiveData、RxJava)等等Paging总是能够胜任分页数据加载的需求。更多 参考再次重申强烈建议 读者将本文作为学习Paging 阅读优先级最高的文章所有其它的Paging中文博客阅读优先级都应该靠后。——是因为本文的篇幅较长吗(1w字的确...)不止如此本文尝试对Paging的整体结构进行拆分笔者认为只要对整体结构有足够的理解一切API的调用都轻而易举。但如果直接上手写代码的话反而容易造成 只见树木,不见森林 之感上手效率反而降低。推荐阅读(点击标题可跳转阅读)App流畅度优化利用字节码插桩实现一个快速排查高耗时方法的工具谈谈Android AOP技术方案代理模式以及在Android中的使用看完这篇 HTTPS和面试官扯皮就没问题了觉得本文对你有帮助请分享给更多人wx号gulinhai531顾林海公众号不定期推出优质文章喜欢的朋友们给我个好看。好文章我在看❤️