反思|Android 列表分页组件Paging的设计与实现:系统概述

前言

本文将对Paging分页组件的设计和实现进行一个系统总体的概述,强烈建议 读者将本文做为学习Paging 阅读优先级最高的文章,全部其它的Paging中文博客阅读优先级都应该靠后。git

本文篇幅 较长,总体结构思惟导图以下:github

1、起源

手机应用中,列表是常见的界面构成元素,而对于Android开发者而言,RecyclerView是实现列表的不二选择。数据库

在正式讨论Paging和列表分页功能以前,咱们首先看看对于一个普通的列表,开发者如何经过代码对其进行建模:api

如图所示,针对这样一个简单 联系人界面 的建模,咱们引出3个重要的层级:缓存

1.服务端组件、数据库、内存

为何说 服务端组件数据库 以及 内存 是很是重要的三个层级呢?服务器

首先,开发者为当前页面建立了一个ViewModel,并经过成员变量在 内存 中持有了一组联系人数据,由于ViewModel组件的缘由,即便页面配置发生了改变(好比屏幕的旋转),数据依然会被保留下来。网络

数据库 的做用则保证了App即便在离线环境下,用户依然能够看到必定的内容——显然对于上图中的页面(联系人列表)而言,本地缓存是很是有意义的。数据结构

对于绝大多数列表而言,服务端 每每意味着是数据源,每当用户执行刷新操做,App都应当尝试向服务端请求最新的数据,并将最新的数据存入 数据库,并随之展现在UI上。架构

一般状况下,这三个层级并不是同时都是必要的,读者需正确理解三者各自不一样的使用场景。app

如今,借助于 服务端组件数据库 以及 内存,开发者将数据展现在RecyclerView上,这彷佛已是正解了。

2.问题在哪?

到目前为止,问题尚未彻底暴露出来。

咱们忽视了一个很是现实的问题,那就是 数据是动态的 ——这意味着,每当数据发生了更新(好比用户进行了下拉刷新操做),开发者都须要将最新的数据响应在UI上。

这意味着,当某个用户的联系人列表中有10000个条目时,每次数据的更新,都会对全部的数据进行重建——从而致使 性能很是低下,用户看到的只是屏幕中的几条联系人信息,为此要从新建立10000个条目?用户显然没法接受。

所以,分页组件的设计势在必行。

3.整理需求

3.一、简单易用

上文咱们谈到,UI响应数据的变动,这种状况下,使用 观察者模式 是一个不错的主意,好比LiveDataRxJava甚至自定义一个接口等等,开发者仅须要观察每次数据库中数据的变动,并进行UI的更新:

class MyViewModel : ViewModel() {
  val users: LiveData<List<User>>
}
复制代码

新的组件咱们也但愿能拥有一样的便利,好比使用LiveData或者RxJava,并进行订阅处理数据的更新—— 简单易用

3.二、处理更多层级

咱们但愿新的组件可以处理多层,咱们但愿列表展现 服务器 返回的数据、 或者 数据库 中的数据,并将其放入UI中。

3.三、性能

新的组件必须保证足够的快,不作任何不必的行为,为了保证效率,繁重的操做不要直接放在UI线程中处理。

3.四、感知生命周期

若是可能,新的组件须要可以对生命周期进行感知,就像LiveData同样,若是页面并不在屏幕的可视范围内,组件不该该工做。

3.五、足够灵活

足够的灵活性很是重要——每一个项目都有不一样的业务,这意味着不一样的API、不一样的数据结构,新的组件必须保证可以应对全部的业务场景。

这一点并不是必须,可是对于设计者来讲难度不小,这意味着须要将不一样的业务中的共同点抽象出来,并保证这些设计适用在任何场景中。

定义好了需求,在正式开始设计Paging以前,首先咱们先来回顾一下,普通的列表如何实现数据的动态更新的。

4.普通列表的实现方式

咱们依然经过 联系人列表 做为示例,来描述普通列表 如何响应数据的动态更新

首先,咱们须要定义一个Dao,这里咱们使用了Room组件用于 数据库 中联系人的查询:

@Dao
interface UserDao {
  @Query("SELECT * FROM user")
  fun queryUsers(): LiveData<List<User>>
}
复制代码

这里咱们返回的是一个LiveData,正如咱们前文所言,构建一个可观察的对象显然会让数据的处理更加容易。

接下来咱们定义好ViewModelActivity:

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.AdapterAsyncListDiffer封装类,其内建立了AsyncListDiffer的示例,以便在后台线程中使用DiffUtil计算新旧数据集的差别,从而节省Item更新的性能。

本文默认读者对ListAdapter必定了解,若是不是很熟悉,请参考DiffUtilAsyncListDifferListAdapter等相关知识点的文章。

此外,咱们还须要在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, 接下来咱们开始思考,新的分页组件应该是什么样的。

2、分页组件简介

1.核心类:PagedList

上文提到,一个普通的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个数据?显然咱们目前的代码没法进行配置,这是不合理的。

2.数据源: DataSource及其工厂

回答这个问题以前,咱们还须要定义一个角色,用来为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中取最新的数据进行展现——固然,这以后的分页流程都是相同的,无需再次复述。

笔者绘制了一幅图用于描述三者之间的关系,读者可参考上述文字和图片加以理解:

3.串联二者:PagedListBuilder

回归第一小节的那个问题,分页相关业务如何进行配置?咱们虽然介绍了为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   
  }
) {
  // ...
}
复制代码

准确的来讲,二者内部的实现还有微弱的区别,前者ListAdaptergetItem()函数的返回值是User,然后者PagedListAdapter返回值应该是User?(Nullable),其缘由咱们会在下面的Placeholder部分进行描述。

4.更多可选配置:PagedList.Config

目前的介绍中,分页的功能彷佛已经实现完毕,但这些在现实开发中每每不够,产品业务还有更多细节性的需求。

在上一小节中,咱们经过LivePagedListBuilderLiveData<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设计来讲,建造者模式 显然是不错的选择。

接下来咱们简单了解一下,这些可选的配置分别表明了什么。

4.1.分页数量:PageSize

最易理解的配置,分页请求数据时,开发者老是须要定义每页加载数据的数量。

4.2.初始加载数量:InitialLoadSizeHint

定义首次加载时要加载的Item数量。

此值一般大于PageSize,所以在初始化列表时,该配置可使得加载的数据保证屏幕能够小范围的滚动。

若是未设置,则默认为PageSize的三倍。

4.3.预取距离:PrefetchDistance

顾名思义,该参数配置定义了列表当距离加载边缘多远时进行分页的请求,默认大小为PageSize——即距离底部还有一页数据时,开启下一页的数据加载。

若该参数配置为0,则表示除非明确要求,不然不会加载任何数据,一般不建议这样作,由于这将致使用户在滚动屏幕时看到占位符或列表的末尾。

4.4.是否启用占位符:PlaceholderEnabled

该配置项须要传入一个boolean值以决定列表是否开启placeholder(占位符),那么什么是placeholder呢?

咱们先来看未开启占位符的状况:

如图所示,没有开启占位符的状况下,列表展现的是当前全部的数据,请读者重点观察图片右侧的滚动条,当滚动到列表底部,成功加载下一页数据后,滚动条会从长变短,这意味着,新的条目成功实装到了列表中。一言以蔽之,未开启占位符的列表,条目的数量和PagedList中数据数量是一致的。

接下来咱们看一下开启了占位符的状况:

如图所示,开启了占位符的列表,条目的数量和DataSource中数据的总量是一致的。 这并不意味着列表从DataSource一次加载了大量的数据并进行渲染,全部业务依然交给Paging进行分页处理。

当用户滑动到了底部还没有加载的数据时,开发者会看到还未渲染的条目,这是理所固然的,PagedList的分页数据加载是异步的,这时对于Item的来讲,要渲染的数据为null,所以开发者须要配置占位符,当数据未加载完毕时,UI如何进行渲染——这也正是为什么上文说到,对于PagedListAdapter来讲,getItem()函数的返回值是可空的User?,而不是User

随着PagedList下一页数据的异步加载完毕,伴随着RecyclerView的原生动画,新的数据会被从新覆盖渲染到placeholder对应的条目上,就像gif图展现的同样。

4.5.关于Placeholder

这里我专门开一个小节谈谈关于placeholder,由于这个机制和咱们传统的分页业务彷佛有所不一样,但Google的工程师们认为在某些业务场景下,该配置确实颇有用。

开启了占位符,用户老是能够快速的滑动列表,由于列表“持有”了整个数据集,所以不会像未开启占位符时,滑动到底部而被迫暂停滚动,直到新的数据的加载完毕才能继续浏览。顺畅的操做总比指望以外的阻碍要好得多

此外,开启了占位符意味着用户与 加载指示器 完全告别,相似一个 正在加载更多... 的提示标语或者一个简陋的ProgressBar效果然的会提高用户体验吗?也许答案是否认的,相比之下,用户应该更喜欢一个灰色的占位符,并等待它被新的数据渲染。

但缺点也随之而来,首先,占位符的条目高度应该和正确的条目高度一致,在某些需求中,这也许并不符合,这将致使渐进性的动画效果并不会那么好。

其次,对于开发者而言,开启占位符意味着须要对ViewHolder进行额外的代码处理,数据为null或者不为null?两种状况下的条目渲染逻辑都须要被添加。

最后,这是一个限制性的条件,您的DataSource数据源内部的数据数量必须是肯定的,好比经过Room从本地获取联系人列表;而当数据经过网络请求获取的话,这时数据的数量是不肯定的,不开启Placeholder反而更好。

5.更多观察者类型的配置

在本文的示例中,咱们创建了一个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()
复制代码

3、工做流程原理概述

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,并对列表进行 增量更新

4、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都可以实现,开发者按需取用便可。

5、最佳实践

如今读者对多种不一样的数据源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:列表状态的响应式管理

6、总结

本文对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

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

相关文章
相关标签/搜索