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