Giter Club home page Giter Club logo

Comments (1)

qingmei2 avatar qingmei2 commented on August 27, 2024

三、工作流程原理概述

Paging幕后是如何工作的?

接下来,笔者将针对Paging分页组件的工作流程进行系统性的描述,探讨Paging如何实现异步分页数据的加载和响应 的。

为了便于理解,笔者将整个流程拆分为三个步骤,并为每个步骤绘制对应的一张流程图,这三个步骤分别是:

  • 1.初次创建流程
  • 2.UI渲染和分页加载流程
  • 3.刷新数据源流程

1.初次创建流程

如图所示,我们定义了ViewModelRepositoryRepository内部实现了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尝试在后台线程中通过DataSourcePagedList内部的数据列表进行初始化。

现在,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对应的PagedListDataSource已经没有失效了,因为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>中的Key对应数据加载的条件,Value对应数据集的实际类型, 针对不同场景,Paging的设计者提供了三种不同类型的DataSource抽象类:

  • PositionalDataSource<T>
  • ItemKeyedDataSource<Key, Value>
  • PageKeyedDataSource<Key, Value>

接下来我们分别对其进行简单的介绍。

本章节涉及的知识点非常重要,但不作为本文的重点,笔者将在该系列的下一篇文章中针对DataSource的设计与实现进行更细节的探究,欢迎关注。

1.PositionalDataSource

PositionalDataSource<T>是最简单的DataSource类型,顾名思义,其通过数据所处当前数据集快照的位置(position)提供数据。

PositionalDataSource<T>适用于 目标数据总数固定,通过特定的位置加载数据,这里KeyInteger类型的位置信息,并且被内置固定在了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找到下一个用户BobA -> B)。

3.PageKeyedDataSource

更多的网络请求API中,服务器返回的数据中都会包含一个String类型类似nextPage的字段,以表示当前页数据的下一页数据的接口(比如GithubAPI),这种分页数据加载的方式正是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请求网络数据,请求成功,数据存入DatabaseViewModel观察到Database中数据的变更,并更新到UI中。

这似乎无法满足上文中的需求?读者认真思考可知,其实是没问题的,当网络连接发生故障时,这时向服务端请求数据失败,并不会更新Database,因此UI展示的正是期望的本地缓存。

ViewModel仅仅响应Database中数据的变更,这种使用 单一数据源 的方式让复杂的业务逻辑简化了很多。

4.分页列表的最佳实践

现在我们理解了 单一数据源 的好处,该方案在分页组件中也同样适用,我们唯一需要实现的是,如何主动触发服务端数据的请求?

这是当然的,因为Database中依赖网络请求成功之后的数据存储更新,否则列表所展示的永远是Database中不变的数据——别忘了,ViewModelServer之间并没有任何关系。

针对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分页加载还有更多好处,比如更轻易管理分页列表 额外的状态

不仅仅是分页列表,这种方案使得所有列表的 状态管理 的更加容易,笔者为此撰写了另外一篇文章去阐述它,篇幅所限,本文不进行展开,有兴趣的读者可以阅读。

Android官方架构组件Paging-Ex:列表状态的响应式管理

六、总结

本文对Paging进行了系统性的概述,最后,Paging到底是一个什么样的分页库?

首先,它支持NetworkDatabase或者两者,通过Paging,你可以轻松获取分页数据,并直接更新在RecyclerView中。

其次,Paging使用了非常优秀的 观察者模式 ,其简单的API的内部封装了复杂的分页逻辑。

第三,Paging灵活的配置和强大的支持——不同DataSource的数据加载方式、不同的响应式库的支持(LiveDataRxJava)等等,Paging总是能够胜任分页数据加载的需求。


更多 & 参考

再次重申,强烈建议 读者将本文作为学习Paging 阅读优先级最高的文章,所有其它的Paging中文博客阅读优先级都应该靠后。

——是因为本文的篇幅较长吗?(1w字的确...)不止如此,本文尝试对Paging的整体结构进行拆分,笔者认为,只要对整体结构有足够的理解,一切API的调用都轻而易举。但如果直接上手写代码的话,反而容易造成 只见树木,不见森林 之感,上手效率反而降低。

此外,本文附带一些学习资料,供读者参考:

1.参考视频

本文的大纲来源于 Google I/O '18中对Paging的一个视频分享,讲的非常精彩,本文绝大多数内容和灵感也是由此而来,强烈建议读者观看。

2.参考文章

其实也就是笔者去年写的几篇关于Paging的文章:


关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

from blogs.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.