Android.Arch.Paging: 分页加载的新选项

3,866 阅读6分钟
原文链接: mp.weixin.qq.com

文:栋栋

本文原创,转载请注明作者及出处

一、概述

在很久很久以前,加载并展示大量数据就已成为各家应用中必不可少的业务场景,分页加载也就成了必不可少的方案。在现有的 Android API 中也已存在支持分页加载内容的方案, 比如:

  • CursorAdapter:它简化了数据库中数据到  ListView中 Item 的映射, 仅查询需要展示的数据,但是查询的过程是在UI线程中执行。

  • SupportV7 包中的 AsyncListUtil支持基于 position 的数据集分页加载到 RecyclerView中,但不支持不基于 position 的数据集,而且它强制一个有限数据集中的 null 项必须展示 Placeholder.

针对现有方案所存在的一些问题,Google 推出了 Android 架构组件中的 Paging Library, 不过目前还是 alpha 版本。Paging Library 主要由 3 个部分组成: DataSource、  PagedList、  PagedListAdapter

二、Paging Libray介绍

DataSourcePagedListPagedAdapter三者之间的关系以及加载数据到展示数据的流程如下图所示:

2.1 Datasource

顾名思义, Datasource<Key,Value>是数据源相关的类,其中  Key对应加载数据的条件信息,  Value对应返回结果, 针对不同场景,Paging 提供了三种 Datasource:

  • PageKeyedDataSource <Key , Value> :适用于目标数据根据页信息请求数据的场景,即  Key 字段是页相关的信息。比如请求的数据的参数中包含类似  next /previous的信息。

  • ItemKeyedDataSource <Key , Value> :适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。

  • PositionalDataSource <T > :适用于目标数据总数固定,通过特定的位置加载数据,这里 Key是Integer类型的位置信息,  T即  Value。 比如从数据库中的1200条开始加在20条数据。

以上三种 Datasource 都是抽象类, 使用时需实现请加载数据的方法。三种Datasource 都需要实现 loadInitial()方法, 各自都封装了请求初始化数据的参数类型 LoadInitialParams。 不同的是分页加载数据的方法, PageKeyedDataSource和  ItemKeyedDataSource比较相似, 需要实现 loadBefore()和  loadAfter () 方法,同样对请求参数做了封装,即 LoadParams<Key>。  PositionalDataSource需要实现  loadRange () ,参数的封装类为 LoadRangeParams

如果项目中使用 Android 架构组件中的 Room, Room 可以创建一个产出 PositionalDataSource的  DataSource .Factory

                                            
  1. @Query ("select * from users WHERE age > :age order by name DESC, id ASC" )

  2. DataSource .Factory< Integer, User> usersOlderThan( int age);

总的来说,Datasource 就像是一个抽水泵,而不是真正的水源,它负责从数据源加载数据,可以看成是 Paging Library 与数据源之间的接口。

2.2 PagedList

如果将 Datasource 比作抽水泵,那 PagedList 就像是一个蓄水池,但不仅仅如此。PagedList 是 List 的子类,支持所有 List 的操作, 除此之外它主要有五个成员:

  • mMainThreadExecutor: 一个主线程的 Excutor, 用于将结果 post 到主线程。 

  • mBackgroundThreadExecutor: 后台线程的 Excutor.

  • BoundaryCallback:加载 Datasource 中的数据加载到边界时的回调.

  •  Config: 配置 PagedList 从 Datasource 加载数据的方式, 其中包含以下属性:

    pageSize :设置每页加载的数量 

    prefetchDistance :预加载的数量 

    initialLoadSizeHint :初始化数据时加载的数量

     enablePlaceholders :当 item 为 null 是否使用 PlaceHolder 展示

  •  PagedStorage<T>: 用于存储加载到的数据,它是真正的蓄水池所在,它包含一个 ArrayList<List<T>> 对象  mPages,按页存储数据。

PagedList 会从 Datasource 中加载数据,更准确的说是通过 Datasource 加载数据, 通过 Config 的配置,可以设置一次加载的数量以及预加载的数量。 除此之外,PagedList 还可以向 RecyclerView.Adapter 发送更新的信号,驱动 UI 的刷新。

2.3 PagedListAdapter

PagedListAdapter 是 RecyclerView.Adapter 的实现,用于展示 PagedList 的数据。它本身实现的更多是 Adapter 的功能,但是它有一个小伙伴 PagedListAdapterHelper<T>, PagedListAdapterHelper 会负责监听 PagedList 的更新, Item 数量的统计等功能。这样当 PagedList 中新一页的数据加载完成时, PagedAdapte 就会发出加载完成的信号,通知 RecyclerView 刷新,这样就省略了每次 loading 后手动调一次 notifyDataChanged().

除此之外,当数据源变动产生新的 PagedList,PagedAdapter 会在后台线程中比较前后两个 PagedList 的差异,然后调用 notifyItem...() 方法更新 RecyclerView。这一过程依赖它的另一个小伙伴 ListAdapterConfig, ListAdapterConfig 负责主线程和后台线程的调度以及 DiffCallback的管理,  DiffCallback的接口实现中定义比较的规则,比较的工作则是由  PagedStorageDiffHelper来完成。

三、加载数据

使用 Paging Library 加载数据主要有两种方式,一种是单一数据源的加载(本地数据或网络数据), 另一种是多个数据源的加载(本地数据+网络数据)。

3.1 加载单一数据源的数据

首先我们可以通过 LivePagedListBuilder来创建  LiveData <PagedList > 为 UI 层提供数据。整个流程如下图所示: 

如果数据源是 DB,当数据发生变化,DB 会推送 (push) 一个新的 PagedList(这里会依赖 LiveData的机制). 如果是网络数据,即客户端无法知道数据源的变化,可以通过诸如滑动刷新的方式调用 Datasource 的 invalidate()方法来拉去 (pull) 新的数据。

3.2 加载多个数据源的数据

这种场景一般是先加载本地数据,加载完成后再加载网络数据,比较适合需要本地做缓存的业务。比如 IM 中的聊天消息,当打开聊天界面时先加载本地数据库中的聊天消息,加载完了再加载网络的离线消息。这中场景的流程如下图所示: 

这种场景需要为 PagedList 设置 BoundaryCallback来监听加载完本地数据的事件,触发加载网络数据,然后入库,此时 LiveData  会推送一个新的 PagedList, 并触发界面刷新。

具体使用案例可以参考 Google Sample 的 PagingWithNetworkSample 项目。

四、小结

Paging Library 作为 Android 架构组件库的一员,其特点主要还是在其架构思想上。Paging 将分页的业务封装为一条完整的流水线,一个 Pattern。其中各个组件之间存在联动的关系:

  • 当 PagedList 创建时会立即从 Datasource 加载数据(触发 loadInitial() ), DataSource 加载到数据后会更新 PagedListPagedList 更新会通知到 PagedAdapter 并刷新UI; 

  • UI 上的展示会触发 PagedAdapter 的 getItem() 随即触发 PagedList 的 loadAround() 方法从 DataSource 加载周围的数据...

整个过程 Paging 内部实现了线程的切换,数据的预加载,所有联动的关系都内聚到Paging 中,这样使用时只需要关心加载数据的具体实现,并且在用户体验上,将会大大减少等待数据加载的时间和次数。

End

推荐阅读

探索Android架构组件Room

浅谈技术栈迁移之测试心得

部署 LanguageTool 到 Servlet

翻译 | 调整JavaScript抽象的迭代方案