Comments (1)
三、工作流程原理概述
Paging
幕后是如何工作的?
接下来,笔者将针对Paging
分页组件的工作流程进行系统性的描述,探讨Paging
是 如何实现异步分页数据的加载和响应 的。
为了便于理解,笔者将整个流程拆分为三个步骤,并为每个步骤绘制对应的一张流程图,这三个步骤分别是:
- 1.初次创建流程
- 2.UI渲染和分页加载流程
- 3.刷新数据源流程
1.初次创建流程
如图所示,我们定义了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
层迈出属于自己的第一个脚印。
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
)进行加载。
通过LiveData
,UI
层级再次观察到了新的PagedList
,并再次通过submitList()
函数注入到PagedListAdapter
中。
和初次的数据渲染不同,这一次我们使用到了PagedListAdapter
内部的AsyncPagedListDiffer
对两个数据集进行差异性计算——这避免了notifyDataSetChanged()
的滥用,同时,差异性计算的任务被切换到了后台线程中执行,一旦计算出差异性结果,新的PagedList
会替换旧的PagedList
,并对列表进行 增量更新。
四、DataSource数据源简介
Paging
分页组件的设计中,DataSource
是一个非常重要的模块。顾名思义,DataSource<Key, Value>
中的Key
对应数据加载的条件,Value
对应数据集的实际类型, 针对不同场景,Paging
的设计者提供了三种不同类型的DataSource
抽象类:
PositionalDataSource<T>
ItemKeyedDataSource<Key, Value>
PageKeyedDataSource<Key, Value>
接下来我们分别对其进行简单的介绍。
本章节涉及的知识点非常重要,但不作为本文的重点,笔者将在该系列的下一篇文章中针对
DataSource
的设计与实现进行更细节的探究,欢迎关注。
1.PositionalDataSource
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") {
// ...
};
}
};
}
2.ItemKeyedDataSource
ItemKeyedDataSource<Key, Value>
适用于目标数据的加载依赖特定条目的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的某些信息时。
同样拿联系人列表举例,另外的一种分页加载方式是通过上一个联系人的name
作为Key
请求新一页的数据,因为联系人name
字母排序的原因,DataSource
很容易针对一个name
检索并提供接下来新一页的联系人数据——比如根据Alice
找到下一个用户Bob
(A -> B
)。
3.PageKeyedDataSource
更多的网络请求API
中,服务器返回的数据中都会包含一个String
类型类似nextPage
的字段,以表示当前页数据的下一页数据的接口(比如Github
的API
),这种分页数据加载的方式正是PageKeyedDataSource<Key, Value>
的拿手好戏。
这是日常开发中用到最多的DataSource
类型,和ItemKeyedDataSource<Key, Value>
不同的是,前者的数据检索关系是单个数据与单个数据之间的,后者则是每一页数据和每一页数据之间的。
同样拿联系人列表举例,这种分页加载方式是按照页码进行数据加载的,比如一次请求15条数据,服务器返回数据列表的同时会返回下一页数据的url
(或者页码),借助该参数请求下一页数据成功后,服务器又回返回下下一页的url
,以此类推。
总的来说,DataSource
针对不同种数据分页的加载策略提供了不同种的抽象类以方便开发者调用,很多情况下,同样的业务使用不同的DataSource
都能够实现,开发者按需取用即可。
五、最佳实践
现在读者对多种不同的数据源DataSource
有了简单的了解,先抛开 分页列表 的业务不谈,我们思考另外一个问题:
当列表的数据通过多个层级 网络请求(
Network
) 和 本地缓存 (Database
)进行加载该怎么处理?
回答这个问题,需要先思考另外一个问题:
Network
+Database
的解决方案有哪些优势?
1.优势
读者认真思考可得,Network
+Database
的解决方案优点如下:
- 1.非常优秀的离线模式支持,即使用户设备并没有链接网络,本地缓存依然可以带来非常不错的使用体验;
- 2.数据的快速恢复,如果异常导致
App
的终止,本地缓存可以对页面数据进行快速恢复,大幅减少流量的损失,以及加载的时间。 - 3.两者的配合的效果总是相得益彰。
看起来Network
+Database
是一个非常不错的数据加载方案,那么为什么大多数场景并没有使用本地缓存呢?
主要原因是开发成本——本地缓存的搭建总是需要额外的代码,不仅如此,更重要的原因是,数据交互的复杂性也会导致额外的开发成本。
2.复杂的交互模型
为什么说Network
+Database
会导致 数据交互的复杂性 ?
让我们回到本文的 联系人列表 的示例中,这个示例中,所有联系人数据都来自 本地缓存,因此读者可以很轻易的构建出该功能的整体结构:
如图所示,ViewModel
中的数据总是由Database
提供,如果把数据源从Database
换成Network
,数据交互的模型也并没有什么区别—— 数据源总是单一的。
那么,当数据的来源不唯一时——即Network
+Database
的数据加载方案中会有哪些问题呢?
我们来看看常规的实现方案的数据模型:
如图所示,ViewModel
尝试加载数据时,总是会先进行网络判断,若网络未连接,则展示本地缓存,否则请求网络,并且在网络请求成功时,将数据保存本地。
乍得一看,这种方案似乎并没有什么问题,实际上却有两个非常大的弊端:
2.1 业务并非这么简单
首先,通过一个boolean
类型的值就能代表网络连接的状态吗?显而易见,答案是否定的。
实际上,在某些业务场景下,服务器的连接状态可以是更为复杂的,比如接收到了部分的数据包?比如某些情况下网络请求错误,这时候是否需要重新展示本地缓存?
若涉及到网络请求的重试则更复杂,成功展示网络数据,再次失败展示缓存——业务越来越复杂,我们甚至会逐渐沉浸其中无法自拔,最终醒悟,这种数据的交互模型完全不够用了 。
2.2 无用的本地缓存
另外一个很明显的弊端则是,当网络连接状态良好的时候,用户看到的数据总是服务器返回的数据。
这种情况下,请求的数据再次存入本地缓存似乎毫无意义,因为网络环境的通畅,Database
中的缓存从来未作为数据源被展示过。
3.使用单一数据源
使用 单一数据源 (single source of truth
)的好处不言而喻,正如上文所阐述的,多个数据源 反而会将业务逻辑变得越来越复杂,因此,我们设计出这样的模型:
ViewModel
如果响应Database
中的数据变更,且Database
作为唯一的数据来源?
其思路是:ViewModel
只从Database
中取得数据,当Database
中数据不够时,则向Server
请求网络数据,请求成功,数据存入Database
,ViewModel
观察到Database
中数据的变更,并更新到UI
中。
这似乎无法满足上文中的需求?读者认真思考可知,其实是没问题的,当网络连接发生故障时,这时向服务端请求数据失败,并不会更新Database
,因此UI
展示的正是期望的本地缓存。
ViewModel
仅仅响应Database
中数据的变更,这种使用 单一数据源 的方式让复杂的业务逻辑简化了很多。
4.分页列表的最佳实践
现在我们理解了 单一数据源 的好处,该方案在分页组件中也同样适用,我们唯一需要实现的是,如何主动触发服务端数据的请求?
这是当然的,因为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
进行分页加载的功能完成了最后一块拼图,现在,分页列表所有数据都来源于本地缓存,并且复杂的业务实现起来也足够灵活。
5.更多优势
通过Network
+Database
进行Paging
分页加载还有更多好处,比如更轻易管理分页列表 额外的状态 。
不仅仅是分页列表,这种方案使得所有列表的 状态管理 的更加容易,笔者为此撰写了另外一篇文章去阐述它,篇幅所限,本文不进行展开,有兴趣的读者可以阅读。
六、总结
本文对Paging
进行了系统性的概述,最后,Paging
到底是一个什么样的分页库?
首先,它支持Network
、Database
或者两者,通过Paging
,你可以轻松获取分页数据,并直接更新在RecyclerView
中。
其次,Paging
使用了非常优秀的 观察者模式 ,其简单的API
的内部封装了复杂的分页逻辑。
第三,Paging
灵活的配置和强大的支持——不同DataSource
的数据加载方式、不同的响应式库的支持(LiveData
、RxJava
)等等,Paging
总是能够胜任分页数据加载的需求。
更多 & 参考
再次重申,强烈建议 读者将本文作为学习Paging
阅读优先级最高的文章,所有其它的Paging
中文博客阅读优先级都应该靠后。
——是因为本文的篇幅较长吗?(1w字的确...)不止如此,本文尝试对Paging
的整体结构进行拆分,笔者认为,只要对整体结构有足够的理解,一切API
的调用都轻而易举。但如果直接上手写代码的话,反而容易造成 只见树木,不见森林 之感,上手效率反而降低。
此外,本文附带一些学习资料,供读者参考:
1.参考视频
本文的大纲来源于 Google I/O '18
中对Paging
的一个视频分享,讲的非常精彩,本文绝大多数内容和灵感也是由此而来,强烈建议读者观看。
2.参考文章
其实也就是笔者去年写的几篇关于Paging
的文章:
- Android官方架构组件Paging:分页库的设计美学
- Android官方架构组件Paging-Ex:为分页列表添加Header和Footer
- Android官方架构组件Paging-Ex:列表状态的响应式管理
关于我
Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github。
如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?
from blogs.
Related Issues (20)
- 反思|Android 事件拦截机制的设计与实现
- 反思|Android源码模块化管理工具Repo分析
- 反思|Android 输入系统 & ANR机制的设计与实现 HOT 1
- 反思 | 事件总线的局限性,及组件化开发流程中通信机制的设计与实现
- 反思|官方也无力回天?Android SharedPreferences的设计与实现
- [译] 编写AndroidStudio插件(一):创建一个基本插件
- [译] 编写AndroidStudio插件(二):持久化数据
- [译] 编写AndroidStudio插件(三): 更多配置
- [译] 编写AndroidStudio插件(四):整合Jira
- [译] 编写AndroidStudio插件(五):本地化和通知
- 反思 | 开启B站少女心模式,探究APP换肤机制的设计与实现
- [译] Android Visualizer 可视化器的自定义实现
- 反思 | Android 音视频缓存机制的系统性设计
- Android 音频倍速的原理与算法分析
- Android ExoPlayer 分场景集成不同音频倍速算法的实现
- 卷起来了!Android OpenGL 仿自如 APP 裸眼 3D 效果
- Unity3D-导出特效到安卓项目流程
- 反思: Google 为何把 SurfaceView 设计的这么难用?
- 目录链接错误 HOT 1
- 社区说|浅谈 WorkManager 的设计与实现:系统概述
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from blogs.