paging 主要作用就是用来加载列表数据以及加载更多也就是分页加载的,展示还是人家 recyclerview 的事儿,只不过 paging 可以把这个过程变得更加的高效。想想咱们以前加载更多怎么做的,我们需要关注到 recyclerview 或者 listview 通过他们知道用户是不是滑动到页面底部了,是不是该加载更多了,并且还需要我们自己维护加载到第几页,下次该加载哪一页,而现在呢?这一切几乎都不需要我们开发者自己维护,全部都交给 paging 吧。
说到 paging,对它很重要的就是它的数据源,也就是 datasource,而 datasource 一般分为三种,其一是只从网络加载数据,其二是只从本地数据库加载数据,其三是当本地数据库没有数据的时候,先从网络加载,然后缓存到本地数据库,而后再从数据库响应到视图层。
其中估计我们最常用的就是直接从网络加载数据,但是官方的示例是上面我写的其三那种情况,是用 retrofit 处理网络请求,然后用 jetpack 里面的 room 数据库做本地缓存,所以我们还需要了解下这种方式,我们知道 room 数据库的 Dao 层可以直接将数据以 LiveData 包装的形式查询出来,但是 pagelist 需要的是 datasource,所以我们需要将 Dao 层的查询结果返回值改为 DataSource.Factory
但是我们知道视图层的数据是由 viewmodel 管理的,而暴露给数据层又是 LiveData,所以现在我们需要做的是将从数据库查询出来的 datasource 转换为视图层可以使用的 LiveData,这时候我们就需要用到 LivePagedListBuilder 了,这是一个对 pagelist 做配置以及提供数据源的建造者,所以它实例化的时候需要传入两个参数,一个是 datasource,另外一个是关于 pagelist 的 Config,这里传入的 datasource 就是我们从数据库查询出来的那个,而 config 需要我们自己构造,我们可以配置分页加载时候每页加载的条数,可以配置如果数据还没加载完成的时候是否有占位等等,这些配置完成之后,我们直接调用 LivePagedListBuilder 的 build 方法就可以得到一个 LiveData
很多人可能会疑问说上面提到的 pagelist 到底是干嘛的呢?它是 pageing 里面的另外一个重要的组件,paging 的分页加载功能也主要是由它实现的,不过本质上它还是一个集合 list,有了数据,我们还需要给 recyclerview 展示所需的 adapter,所以我们要把平时继承 RecyclerView.Adapter 的改为继承 PagedListAdapter,这是一个可以和 pagelist 配合使用的 adapter,这个 adapter 与普通的 adapter 的不同之处还在于它的构造方法还需要一个 DiffUtil.ItemCallback 这是干啥用的呢?
顾名思义就是用来计算不同的,它可以计算出两个 pagelist 之间的不同,然后把那些不同更新到界面上,而不是暴力的将原来的所有数据替换,将整个页面刷新,所以这种 diff 操作能够减少资源的消耗。但是这个不同是怎么定义的呢?还是需要我们业务来定义,所以我们需要实现 ItemCallback 这个抽象类里面的 areItemsTheSame 和 areContentsTheSame 方法,前者用来定义我们用 pagelist 里面的哪个属性来判断两个 item 是否相同,而后者是直接用两个 item 的内存地址来判断是否相同。
以上就是 PagedListAdapter 与普通的 adapter 的不同之处。可是写到这里也只是实现了一个普通的展示一个普通列表的功能,那分页加载功能如何实现呢?这时候就不得不提到 PagedList.BoundaryCallback 了。它会告诉我们什么时候是初始化数据,什么时候要加载更多。我们主要重写 BoundaryCallback 里面的两个方法,其中的 onZeroItemsLoaded 告诉我们数据库中没有数据,代表首次加载,当然也可能是数据库被用户清除情况下的首次加载,onItemAtEndLoaded 则会在数据库中最后一条数据返回后回调,告诉我们需要再次从网络加载数据了。
那么 BoundaryCallback 怎么与 pagelist 结合起来呢?可以把它当做 LivePagedListBuilder 的一个属性给设置进去,这样 BoundaryCallback 就可以响应到数据库里面的变化,进而触发网络请求。到这里可能会有人有疑惑,就是网络请求回来了,插入了数据库,然后我们怎么通知界面数据有变化了呢?如果按照咱们不使用 jetpack 组件的逻辑来说,可能会发个通知,让界面再查询一遍数据库,或者加个接口回调之类的,但是有了 jetpack 里面的 room,这些都不用了,因为咱们上面说了 LivePagedListBuilder 最终会构造出来一个 LiveData 给界面使用,而在每次我们插入数据的时候,这个给这个 LiveData 添加的 observer 就会响应,本质上算观察者模式,这样界面就能够响应到数据库中数据的变化。
至此我们最开始提到的第三种数据库 + 网络请求的模式就算说完了。不过我们平时需求中这种模式使用的并不多,更多的还是直接从网络请求回来数据,这也就意味着我们的 datasource 变了,我们的 datasource 没法直接由数据库给我们提供了,我们需要自己构造 datasource。自定义 datasource,我们可以继承 PageKeyedDataSource、ItemKeyedDataSource 以及 PositionalDataSource。这三种有什么区别呢?PageKeyedDataSource 顾名思义就是以 page 作为 key 的 datasource 例如我们平时的上拉加载更多分页加载就适合使用它,因为我们知道 pageId 或者要加载哪一页就好了。而 ItemKeyedDataSource 意思则是每一下次的请求总会依赖上一次的请求结果,不像 PageKeyedDataSource 每次加载之间都那么独立。而第三种 PositionalDataSource 意为我们可以从任意位置处加载数据,还没有想到应用场景。这三种中小胖觉得也就 PageKeyedDataSource 会用的比较多。
接下来我们就以最常用的 PageKeyedDataSource 来构造 datasource,来跟大家演示如何直接从网络加载数据实现分页加载,我们继承了 PageKeyedDataSource 之后主要重写两个方法,loadInitial 这个方法调用的时候代表是首次从网络请求数据,类似于 BoundaryCallback 里面的 onZeroItemsLoaded,关键的地方在当网络请求成功的时候我们需要将数据响应到界面上,这里要做的操作就是使用 loadInitial 里面的 LoadInitialCallback 的 onResult 方法进行回调,回调了之后就会将数据同步到 pagelist。loadAfter 方法调用的时候代表上滑加载更多被触发了,需要加载更多数据了,从网络请求完数据之后,会通过 LoadCallback 的 onResult 进行回调,进而将数据同步到 pagelist。
经过以上步骤我们只是做了一个 datasource 的实现,具体的 DataSource 的构造还得通过继承 DataSource.Factory 然后在其 create 方法里面进行实例化,而后再将这个 datasource 传入 LivePagedListBuilder 即可,至此之后就跟上面讲的第三种模式一样了,adapter 啥的也不用动。但是这里实现的单纯从网络加载数据的功能只是一个正常功能,好比那种没有数据的情况怎么处理?没有更多数据的怎么处理?这些依然需要开发者自己来处理,当在 loadInitial 里面发出的网络请求返回的数据为空,就代表没有数据,当在 loadAfter 里面发出的网络请求返回的数据为空,则代表没有更多数据。开发者可以将这些事件通过 LiveData 这种形式响应到视图层。
一些需要特殊注意的点:
1、我们知道无论 datasource 是数据库 + 网络请求还是只有网络请求,我们最终都会通过 LivePagedListBuilder 构造出一个 LiveData 给到视图层,但是在这两种模式下添加在 LiveData 上的 observer 响应的频率却是不同的,直接 datasource 是数据库的那种,只要数据库中的数据有变化,那么 LiveData 对应的 observer 就会收到响应,也就是说每次界面要更新都必须通过此 LiveData 的 observer 操作 adapter,具体操作就是 adapter.submitList(),里面传入的是 pagelist。
而直接 datasource 是网络请求跟这个是不一样的,它只会在那个 LiveData 首次被添加 observer 也就是变为 active 也就是 onActive 的时候会被响应一次,在这里将 LiveData 携带过来的 pagelist 与 adapter 绑定,而后数据有任何变化都不会响应到 这个 LiveData 的 observer 了,那数据有变化了,怎么响应到界面上呢?
这就得提到咱上面说的 loadInitial 和 loadAfter 了,我们上面说了在这俩方法网络请求成功之后会调用对应的 callback 的 onResult 方法,通过打断点会看到最终会调用到 pagelist 所在的 adapter 的 notify 方法,所以也不需要我们自己在 observer 里面 submitList 了,换言之从数据的获取到数据响应到 UI 上,都由 pagelist 来完成。
2、我们在 loadInitial 里面调用 callback 的时候可以选择是否传入 totalCount,就是利用 totalCount 告诉 pagelist 所加载列表的总条目数,那么告诉 pagelist 总条目数有什么用呢?这需要结合 PagedList.Config 的 mEnablePlaceholders 属性来看,当我们 callback 的时候传入了 totalCount,且 setEnablePlaceholders(true),那么我们进入列表页面的时候,给我们的感觉就是所有的数据已经加载了,如果 recyclerview 的 scrollbar 展示的话,会发现 scrollbar 比较短,但其实此时也只是加载了一次数据而已,之所以会有加载了所有数据的感觉,是因为我们使用了占位符,当我们快速滑动页面的时候会看到那些还未加载的条目上面都是我们设置的默认值,只是当数据加载完成的时候默认值会被替换为实际的值。设置占位的目的也是为了减少页面加载数据时候的跳动感。
3、这个严格来说是关于 LiveData 的,咱们上面说过第三种模式也就是数据库 + 网络请求的,假设有这么个需求如果一旦数据库中已经有了缓存,就不再从网络请求加载了,这个该怎么实现呢?我们知道目前情况下我们从 room 数据库里面查询出来的是 LiveData,可能很多人也跟我一样第一想法就是直接拿这个 LiveData 来做判断,因为我们可以通过 livedata.getValue 来获取存储在其里面的值,这里来说就是从数据库中查询出来的 list,只要判断这个 list 为 null 或者长度为 0 不就好了吗?想法是完美的,可是现实是残酷的,对于 LiveData 是不能用直接获取值的方式来做任何判断的,因为 LiveData 的赋值是异步的,也就是说我们只能够在对 LiveData 添加的 observer 里面获取其值,也只有在这地方才能够获取到它真正的值,通过我们一开说的那种直接获取值的方式一定是 null,即使已经赋值完成。
那我们知道了只能在 observer 里面来做判断了,可是这不是又把数据加载的逻辑放到了视图层吗?因为我们只在 activity 里面有对这个 LiveData 添加 observer,那怎么办?我们也不能在数据获取层给 LiveData 添加 observer,我们只能在数据库的 Dao 层在写一条 sql,可以直接把数据库中符合条件的数据的 count 查出来,然后进行判断就好了。
但是还有个注意点就是关于 room 数据的操作必须都得在非 UI 线程,换言之就是只要从数据库中查询出来的不是 LiveData,那么就需要我们自己切换线程。