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

本文已受权「玉刚说」微信公众号独家发布android

2019/12/24 补充

距本文发布时隔一年,笔者认为,本文不该该做为入门教程的博客系列,相反,读者真正想要理解 Paging 的使用,应该先尝试理解其分页组件的本质思想:git

反思|Android 列表分页组件Paging的设计与实现:系统概述
反思|Android 列表分页组件Paging的设计与实现:架构设计与原理解析github

以上两篇文章将对Paging分页组件进行了系统性的概述,笔者强烈建议 读者将以上两篇文章做为学习 Paging 阅读优先级 最高 的学习资料,全部其它的Paging中文博客阅读优先级都应该靠后数据库

本文及相关引伸阅读:编程

Android官方架构组件Paging:分页库的设计美学
Android官方架构组件Paging-Ex:为分页列表添加Header和Footer
Android官方架构组件Paging-Ex:列表状态的响应式管理缓存

概述

PagingGoogle在2018年I/O大会上推出的适用于Android原生开发的分页库,随着愈来愈多的开发者着手使用Paging,愈来愈多的问题暴露出来,最直接的一个问题是:服务器

如何管理列表额外的状态?微信

这样的需求随处可见,好比 侧滑删除为评论点赞 等等:markdown

本文将阐述:如何管理Paging分页列表的 状态,为什么这样设计,以及设计的过程。网络

列表的状态问题

和市面上其它热门的分页库相比,Paging最大的亮点在于其 将列表分页加载的逻辑做为回调函数封装入 DataSource,开发者在配置完成后,无需经过代码手动控制分页的加载,列表会 自动加载 下一页数据并展现。

这种便利意味着开发者不须要本身持有 数据源 ,大多数时候这使得开发流程更加便利,但总有偶然,好比这样一个界面:

这种需求家常便饭,其本质是,列表自己展现服务端返回的列表数据以外,还须要 本地控制额外的状态

什么叫 额外的状态 ? 咱们先用简单的一张图展现没有额外状态的情形,这时,列表的全部UI元素都从服务端获取:

如今咱们将上文Gif中的点赞效果也经过一张图表示:

读者可能还未认识到两种业务场景之间的差别性:对于列表的初始化来说,全部UI元素都被服务端返回的数据渲染,每条评论是否已经被点赞,服务端都经过Comment进行了描述。

须要注意的是,在某一刻,用户发现某个评论很是有趣,所以他选择对该评论进行了点赞的操做。

在业务代码中,咱们须要向服务端POST一个点赞的请求,服务端返回了一个200的成功码,但问题来了,接下来咱们 如何让列表中的那条评论状态发生变化(即点赞的icon由灰色变成绿色高亮,已告知用户点同意功)?

这就引起了文章最开始的那个问题,当列表的状态发生了变动,如何管理并更新列表?

方案1:再次刷新请求接口

最简单的方案是再次请求API,每当列表状态发生了变动,从新拉取评论列表,服务端返回的最新数据中,该评论天然已经被点赞了(即列表正确进行了更新)。

读者应该清楚,该方案实际并不可行,缘由有二:

  • 成本过高:某些操做对于用户来讲,应该是很是 轻量级 的(好比点赞),他们甚至但愿这些操做可以 当即被响应 在UI上 ,而请求API并刷新列表这一个过程过重了,即便不考虑服务器的负担,对于用户来讲,UI的刷新须要数秒的等待也是很是糟糕的体验。
  • 不符合逻辑:咱们更须要注意的是,Paging是一个分页列表,而刷新请求行为对于分页列表来讲,是一个不符合产品预期的行为(好比,个人点赞操做是针对第5页的某个评论执行的,产品的设计不可能容许每次点赞都重置为列表的第一页数据,这意味着极度糟糕的用户体验)。

如今咱们理解了 每当列表状态发生了变动就刷新接口 并不是良策,由于这种经过 远程从新拉取数据源 更新UI的方式成本过高了。

方案2:额外维护一个状态的列表

大概思路是在内存中为RecyclerView维护一个额外的List,用于一一映射对应positionItem状态:

class CommentPagedAdapter(
  private val likedList: ArrayList<Boolean>
)
复制代码

经过在内存中维护这样一个List,的确能够实现需求,但读者须要认识到的是,Paging分页库自己最大的优势即是 随着列表的滚动自动加载分页数据,每次分页的行为开发者并不须要手动配置,并经过调用相似notifyItemRangeInserted()的方法更新UI。

很显然,每当分页数据获取后,开发者依然须要手动维护这个额外状态的List——方案2和选择使用Paging的初衷背道而驰,所以它并不是最优先考虑的方案。

库自己设计的问题?

如今问题是,既不能经过 服务端 做为数据源,也不能在 内存中 额外维护一个状态的列表, 读者不免会质疑Paging库自己设计的问题。

我该如何控制列表额外的状态(包括修改、增长或者删除)?

事实上该问题已经在Github的这个 issue 中进行了讨论,Google的工程师的回复是:

从技术的角度而言 ,咱们能够建立一个容许部分更改数据源的API,但以后咱们须要记录这些改动并在主线程上从新传递给列表。这种方法的问题在于,若是你有一个已中止的RecyclerView(也就是后堆栈),它将不会(也不该该)接收任何更新,所以PagedList将保留这个可能很长的数据列表并从新应用于主线程上的每一个观察者。

这使问题变得很是复杂,这就是咱们使用单个列表的缘由。

显然,Paging考虑到了更多,和市面上 什么都能作 的框架相比,它 勇于收紧开发者API的调用权限,在开发者们发挥更多奇思妙想以前,将其牢牢束缚到了可控制的范围以内,这也是笔者很是推崇Paging的缘由之一。

那么咱们该如何处理咱们的业务?此时引入一个新的角色彷佛是一个不错的选择,那就是 持久层(即缓存)。

经过架构解决业务问题

综上所述,对于分页列表的状态管理问题,须要作到的是:

  • 1.将一个单独的List交给Paging去进行分页加载并渲染(不该在内存中手动维护一个额外状态的列表);
  • 2.不该该每次都经过重的操做刷新数据源(好比网络请求刷新接口)。

所以,咱们须要一个 中间件 进行业务的调度——在须要刷新整个数据源的时候(好比用户的下拉刷新操做),从服务端拉取数据;在不须要繁重的操做时(好比用户针对某个评论进行点赞),仅仅须要针对单个数据源进行简单的修改。

这已经不仅仅是业务业务的问题,而且涉及到了项目自己的架构,接下来, 持久层 (即本地缓存)闪亮登场。

1.用持久层做为惟一的数据源

Android平台的数据库框架有不少种,本文以官方的架构组件Room为例。

为何要为项目的架构额外添加一个持久层?事实上,随着项目体系的日益庞大,数据库是终究须要添加进入项目中的,所以,在设计项目的架构以前,提早将数据库的框架配置进来是一个不错的选择——未雨绸缪总不是坏事。

以列表的渲染为例,让咱们来看看项目以前的结构:

回到本文,对于Paging来说,咱们并没有法直接获取数据源,所以对于列表状态的管理,咱们须要额外的角色帮助,那就是本地的持久化缓存。

让咱们看看添加了持久层以后的结构:

添加了缓存以后,每当咱们尝试初始化一个分页列表,框架会从服务器拉取数据,以后数据被存储到了Room中。

请注意!Paging原生提供了对Room数据库框架的支持,所以它老是能够第一时间响应到数据库中数据的变化,并自动渲染在UI上

如今,咱们将 请求服务器API数据的渲染 二者经过持久层进行了隔离,对于RecyclerView来讲,持久层是惟一的数据源,即:

列表只反应了数据库的变动。

如今列表的显示和服务端的请求已经 彻底无关 了,读者也许会有这样的疑问——这样作的好处是什么?

2.列表状态的管理

如今咱们回到文中最初的问题,如何管理列表的状态?

对于一个拥有复杂状态的分页列表,不管是 服务端 做为数据源,仍是在 内存中 额外维护一个状态列表,都不是很好的选择;而如今咱们加入了Room,并做为列表惟一的数据源,局势发生了怎样微妙的变化呢?

让咱们来看看加入了持久层以后,下拉刷新的逻辑发生了怎样的变化:

  • 1.下拉刷新意味着咱们须要重置数据,所以咱们手动清除了数据库内对应表中的数据;
  • 2.当表中数据被清空时,Paging会自动响应到数据的变化,由于没有了数据,因此Paging会自动向服务器请求数据;
  • 3.数据返回后,会再次将数据存储到数据库中;
  • 4.这时Paging会再次响应到数据库的变化,并将最新的数据渲染到UI上。

看起来逻辑复杂了不少,实际上读者须要明确的是,步骤二、三、4都是咱们做为开发者在初始化Paging时就配置好的,所以若是用户须要刷新页面,只须要进行第一步的操做便可,即相似这样的一行代码:

// 刷新操做,仅需清除表内的列表数据
fun swipeRefresh() {
  // 运行一个事务
  db.runInTransaction {
      // 清除列表数据
      db.getDao().clearDataList()
  }
}
复制代码

如今咱们将整个流程中,Paging自动执行的步骤用紫色标记出来:

瞧,除了咱们手动执行的逻辑,全部流程都交给了Paging响应式 地执行。

咱们老是下意识认为复杂的业务逻辑用过程式的编码更容易实现,Paging用事实证实了并不是如此——若是说项目中的某个页面追加了下拉刷新的需求,过程式的编码也许会花费更多的时间,而且代码也许会更分散、啰嗦且易出错。

3.更灵活、且可高度扩展

接下来分析的是,对分页列表点赞这种相对 轻量级的行为 又该如何处理?

答案呼之欲出, 咱们依然用熟悉的流程图表示代码的执行步骤:

即便是复杂的状态,在这种模式下也再也不是难题:首先,咱们将数据库对应表中对应评论的isLike(是否被点赞)设置为true

// 1.对本地的评论数据点赞
fun likeCommentLocal(comment: Comment) {
  // 更新评论
  comment.isLike = true
  // 将评论更新到数据库中
  db.runInTransaction {
     db.getDao().updateLikeComment(o)
  }
}
复制代码

与此同时,咱们也向服务器请求接口,告知评论被用户点赞:

// 2.对评论点赞
fun likeCommentRemote(commentId: String) {
  service.likeComment(commentId)
  // ....
}
复制代码

当数据库中数据发生了变动,Paging仍然会响应到数据的更新,并第一时间更新了UI,同时咱们也向服务器发起了请求,一个完整的 点赞 操做相关的业务代码实现完毕。

有了持久层做为中间件,代码组织的灵活性大大提高,同时也具有了更高的扩展性。列表状态的管理再也不是问题,诸如 点赞下拉刷新侧滑删除 等等等等,均可以经过对持久层的数据源进行修改,paging老是能够第一时间自动响应到变动并更新UI。

也正如Room官方文档第一句话所说的,对于Paging分页列表(对app也同样)复杂的状态的展现和管理,开发者应该 将缓存做为列表的惟一真实的数据源

This cache, which serves as your app's single source of truth.

代码示例?

如读者所看到的,本文尽可能避免展现大篇幅的业务代码,缘由有二:

  • 1.这会破坏文章总体思路的完整性,没有人喜欢阅读大篇幅、连续的代码片断;
  • 2.实际开发中,项目的业务不一样、架构选型不一样,代码的实现方式也不尽相同,所以业务级别的代码展现没有意义。

好比,对于持久层框架的选型,RoomGreenDaoDBFlow都是很是优秀的框架,对于业务代码的实现,RxJavaLiveData、协程都是优秀的实现方案...

本文的目的是阐述笔者遇到问题的解决步骤和思路,读者了解总体的方案以后,能够根据实际项目进行技术选型。

固然,若是有相关的疑惑,欢迎参考下面两个项目的具体实现,这是笔者基于上文的Paging+Room组件,实现了一个简单的Github的客户端,本文不细述。

1.MVVM架构的Sample: github.com/qingmei2/MV…

2.MVI架构的Sample:github.com/qingmei2/MV…

系列文章

争取打造 Android Jetpack 讲解的最好的博客系列

Android Jetpack 实战篇


关于我

Hello,我是却把清梅嗅,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人博客或者Github

若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?

相关文章
相关标签/搜索