本文已受权「玉刚说」微信公众号独家发布android
距本文发布时隔一年,笔者认为,本文不该该做为入门教程的博客系列,相反,读者真正想要理解 Paging 的使用,应该先尝试理解其分页组件的本质思想:git
反思|Android 列表分页组件Paging的设计与实现:系统概述
反思|Android 列表分页组件Paging的设计与实现:架构设计与原理解析github
以上两篇文章将对Paging分页组件进行了系统性的概述,笔者强烈建议 读者将以上两篇文章做为学习 Paging 阅读优先级 最高 的学习资料,全部其它的Paging中文博客阅读优先级都应该靠后。编程
本文及相关引伸阅读:微信
Android官方架构组件Paging:分页库的设计美学
Android官方架构组件Paging-Ex:为分页列表添加Header和Footer
Android官方架构组件Paging-Ex:列表状态的响应式管理markdown
Paging
是Google
在2018年I/O大会上推出的适用于Android
原生开发的分页库,若是您还不是很了解这个 官方钦定 的分页架构组件,欢迎参考笔者的这篇文章:架构
Android官方架构组件Paging:分页库的设计美学app
笔者在实际项目中已经使用Paging
半年有余,和市面上其它热门的分页库相比,Paging
最大的亮点在于其 将列表分页加载的逻辑做为回调函数封装入 DataSource
中,开发者在配置完成后,无需控制分页的加载,列表会 自动加载 下一页数据并展现。框架
本文将阐述:为使用了Paging
的列表添加Header
和Footer
的整个过程、这个过程当中遇到的一些阻碍、以及本身是如何解决这些阻碍的——若是您想直接浏览最终的解决方案,请直接翻阅本文的 最终的解决方案 小节。ide
为RecyclerView
列表添加Header
或Footer
并非一个很麻烦的事,最简单粗暴的方式是将RecyclerView
和Header
共同放入同一个ScrollView
的子View
中,但它无异于对RecyclerView
自身的复用机制视而不见,所以这种解决方案并不是首选。
更适用的解决方式是经过实现 多类型列表(MultiType),除了列表自己的Item
类型以外,Header
或Footer
也被视做一种Item
,关于这种方式的实现网上已有不少文章讲解,本文不赘述。
在正式开始本文内容以前,咱们先来看看最终的实现效果,咱们为一个Student
的分页列表添加了一个Header
和Footer
:
实现这种效果,笔者最初的思路也是经过 多类型列表 实现Header
和Footer
,可是很快咱们就遇到了第一个问题,那就是 咱们并无直接持有数据源。
对于常规的多类型列表而言,咱们能够轻易的持有List<ItemData>
,从数据的控制而言,我更倾向于用一个表明Header
或者Footer
的占位符插入到数据列表的顶部或者底部,这样对于RecyclerView
的渲染而言,它是这样的:
正如我所标注的,List<ItemData>
中一个ItemData
对应了一个ItemView
——我认为为一个Header
或者Footer
单首创建对应一个Model
类型是彻底值得的,它极大加强了代码的可读性,并且对于复杂的Header
而言,表明状态的Model
类也更容易让开发者对其进行渲染。
这种实现方式简单、易读而不失优雅,可是在Paging
中,这种思路一开始就被堵死了。
咱们先看PagedListAdapter
类的声明:
// T泛型表明数据源的类型,即本文中的 Student public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { // ... } 复制代码
所以,咱们须要这样实现:
// 这里咱们只能指定Student类型 class SimpleAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) { // ... } 复制代码
有同窗提出,咱们能够将这里的Student
指定为某个接口(好比定义一个ItemData
接口),而后让Student
和Header
对应的Model
都去实现这个接口,而后这样:
class SimpleAdapter : PagedListAdapter<ItemData, RecyclerView.ViewHolder>(diffCallback) { // ... } 复制代码
看起来确实可行,可是咱们忽略了一个问题,那就是本小节要阐述的:
咱们并无直接持有数据源。
回到初衷,咱们知道,Paging
最大的亮点在于 自动分页加载,这是观察者模式的体现,配置完成后,咱们并不关心 数据是如何被分页、什么时候被加载、如何被渲染 的,所以咱们也不须要直接持有List<Student>
(实际上也持有不了),更无从谈起手动为其添加HeaderItem
和FooterItem
了。
以本文为例,实际上全部逻辑都交给了ViewModel
:
class CommonViewModel(app: Application) : AndroidViewModel(app) { private val dao = StudentDb.get(app).studentDao() fun getRefreshLiveData(): LiveData<PagedList<Student>> = LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder() .setPageSize(15) //配置分页加载的数量 .setInitialLoadSizeHint(40) //初始化加载的数量 .build()).build() } 复制代码
能够看到,咱们并未直接持有List<Student>
,所以list.add(headerItem)
这种 持有并修改数据源 的方案几乎不可行(较真而言,实际上是可行的,可是成本太高,本文不深刻讨论)。
接下来我针对直接实现多类型列表进行尝试,咱们先不讨论如何实现Footer
,仅以Header
而言,咱们进行以下的实现:
class HeaderSimpleAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) { // 1.根据position为item分配类型 // 若是position = 1,视为Header // 若是position != 1,视为普通的Student override fun getItemViewType(position: Int): Int { return when (position == 0) { true -> ITEM_TYPE_HEADER false -> super.getItemViewType(position) } } // 2.根据不一样的viewType生成对应ViewHolder override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { ITEM_TYPE_HEADER -> HeaderViewHolder(parent) else -> StudentViewHolder(parent) } } // 3.根据holder类型,进行对应的渲染 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is HeaderViewHolder -> holder.renderHeader() is StudentViewHolder -> holder.renderStudent(getStudentItem(position)) } } // 4.这里咱们根据StudentItem的position, // 获取position-1位置的学生 private fun getStudentItem(position: Int): Student? { return getItem(position - 1) } // 5.由于有Header,item数量要多一个 override fun getItemCount(): Int { return super.getItemCount() + 1 } // 省略其余代码... // 省略ViewHolder代码 } 复制代码
代码和注释已经将个人我的思想展现的很清楚了,咱们固定一个Header
在多类型列表的最上方,这也致使咱们须要重写getItemCount()
方法,而且在对Item
进行渲染的onBindViewHolder()
方法中,对Sutdent
的获取进行额外的处理——由于多了一个Header,致使产生了数据源和列表的错位差—— 第n个数据被获取时,咱们应该将其渲染在列表的第n+1个位置上。
我简单绘制了一张图来描述这个过程,也许更加直观易懂:
代码写完后,直觉告诉我彷佛没有什么问题,让咱们来看看实际的运行效果:
Gif也许展现并不那么清晰,简单总结下,问题有两个:
Header
更应该是一个静态独立的组件,但实际上它也是列表的一部分,所以白光一闪,除了Student
列表,Header
做为Item
也进行了刷新,这与咱们的预期不符;致使这两个问题的根本缘由仍然是Paging
计算列表的position
时出现的问题:
对于问题1,Paging
对于列表的刷新理解为 全部Item的刷新,所以一样做为Item
的Header
也没法避免被刷新;
问题2依然也是这个问题致使的,在Paging
获取到第一页数据时(假设第一页数据只有10条),Paging
会命令更新position in 0..9
的Item
,而实际上由于Header
的关系,咱们是指望它可以更新第position in 1..10
的Item
,最终致使了刷新以及对新数据的展现出现了问题。
正如标题而言,我尝试求助于Google
和Github
,最终找到了这个连接:
PagingWithNetworkSample - PagedList RecyclerView scroll bug
若是您简单研究过PagedListAdapter
的源码的话,您应该了解,PagedListAdapter
内部定义了一个AsyncPagedListDiffer
,用于对列表数据的加载和展现,PagedListAdapter
更像是一个空壳,全部分页相关的逻辑实际都 委托 给了AsyncPagedListDiffer
:
public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { final AsyncPagedListDiffer<T> mDiffer; public void submitList(@Nullable PagedList<T> pagedList) { mDiffer.submitList(pagedList); } protected T getItem(int position) { return mDiffer.getItem(position); } public int getItemCount() { return mDiffer.getItemCount(); } public PagedList<T> getCurrentList() { return mDiffer.getCurrentList(); } } 复制代码
虽然Paging
中数据的获取和展现咱们是没法控制的,但咱们能够尝试 瞒过 PagedListAdapter
,即便Paging
获得了position in 0..9
的List<Data>
,可是咱们让PagedListAdapter
去更新position in 1..10
的item不就能够了嘛?
所以在上方的Issue
连接中,onlymash 同窗提出了一个解决方案:
重写PagedListAdapter
中被AsyncPagedListDiffer
代理的全部方法,而后实例化一个新的AsyncPagedListDiffer
,并让这个新的differ代理这些方法。
篇幅所限,咱们只展现部分核心代码:
class PostAdapter: PagedListAdapter<Any, RecyclerView.ViewHolder>() { private val adapterCallback = AdapterListUpdateCallback(this) // 当第n个数据被获取,更新第n+1个position private val listUpdateCallback = object : ListUpdateCallback { override fun onChanged(position: Int, count: Int, payload: Any?) { adapterCallback.onChanged(position + 1, count, payload) } override fun onMoved(fromPosition: Int, toPosition: Int) { adapterCallback.onMoved(fromPosition + 1, toPosition + 1) } override fun onInserted(position: Int, count: Int) { adapterCallback.onInserted(position + 1, count) } override fun onRemoved(position: Int, count: Int) { adapterCallback.onRemoved(position + 1, count) } } // 新建一个differ private val differ = AsyncPagedListDiffer<Any>(listUpdateCallback, AsyncDifferConfig.Builder<Any>(POST_COMPARATOR).build()) // 将全部方法重写,并委托给新的differ去处理 override fun getItem(position: Int): Any? { return differ.getItem(position - 1) } // 将全部方法重写,并委托给新的differ去处理 override fun submitList(pagedList: PagedList<Any>?) { differ.submitList(pagedList) } // 将全部方法重写,并委托给新的differ去处理 override fun getCurrentList(): PagedList<Any>? { return differ.currentList } } 复制代码
如今咱们成功实现了上文中咱们的思路,一图胜千言:
上一小节的实现方案是彻底可行的,但我我的认为美中不足的是,这种方案 对既有的Adapter
中代码改动过大。
我新建了一个AdapterListUpdateCallback
、一个ListUpdateCallback
以及一个新的AsyncPagedListDiffer
,并重写了太多的PagedListAdapter
的方法——我添加了数十行分页相关的代码,但这些代码和正常的列表展现并无直接的关系。
固然,我能够将这些逻辑都抽出来放在一个新的类里面,但我仍是感受我 好像是模仿并重写了一个新的PagedListAdapter
类同样,那么是否还有其它的思路呢?
最终我找到了这篇文章:
Android RecyclerView + Paging Library 添加头部刷新会自动滚动的问题分析及解决
这篇文章中的做者经过细致分析Paging
的源码,得出了一个更简单实现Header
的方案,有兴趣的同窗能够点进去查看,这里简单阐述其原理:
经过查看源码,以添加分页为例,Paging
对拿到最新的数据后,对列表的更新实际是调用了RecyclerView.Adapter
的notifyItemRangeInserted()
方法,而咱们能够经过重写Adapter.registerAdapterDataObserver()
方法,对数据更新的逻辑进行调整:
// 1.新建一个 AdapterDataObserverProxy 类继承 RecyclerView.AdapterDataObserver class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver { RecyclerView.AdapterDataObserver adapterDataObserver; int headerCount; public ArticleDataObserver(RecyclerView.AdapterDataObserver adapterDataObserver, int headerCount) { this.adapterDataObserver = adapterDataObserver; this.headerCount = headerCount; } @Override public void onChanged() { adapterDataObserver.onChanged(); } @Override public void onItemRangeChanged(int positionStart, int itemCount) { adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount); } @Override public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload); } // 当第n个数据被获取,更新第n+1个position @Override public void onItemRangeInserted(int positionStart, int itemCount) { adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount); } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount); } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount); } } // 2.对于Adapter而言,仅需重写registerAdapterDataObserver()方法 // 而后用 AdapterDataObserverProxy 去作代理便可 class PostAdapter extends PagedListAdapter { @Override public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) { super.registerAdapterDataObserver(new AdapterDataObserverProxy(observer, getHeaderCount())); } } 复制代码
咱们将额外的逻辑抽了出来做为一个新的类,思路和上一小节的十分类似,一样咱们也获得了预期的结果。
通过对源码的追踪,从性能上来说,这两种实现方式并无什么不一样,惟一的区别就是,前者是针对PagedListAdapter
进行了重写,将Item
更新的代码交给了AsyncPagedListDiffer
;而这种方式中,AsyncPagedListDiffer
内部对Item
更新的逻辑,最终仍然是交给了RecyclerView.Adapter
的notifyItemRangeInserted()
方法去执行的——二者本质上并没有区别。
虽然上文只阐述了Paging library
如何实现Header
,实际上对于Footer
而言也是同样,由于Footer
也能够被视为另一种的Item
;同时,由于Footer
在列表底部,并不会影响position
的更新,所以它更简单。
下面是完整的Adapter
示例:
class HeaderProxyAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) { override fun getItemViewType(position: Int): Int { return when (position) { 0 -> ITEM_TYPE_HEADER itemCount - 1 -> ITEM_TYPE_FOOTER else -> super.getItemViewType(position) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { ITEM_TYPE_HEADER -> HeaderViewHolder(parent) ITEM_TYPE_FOOTER -> FooterViewHolder(parent) else -> StudentViewHolder(parent) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is HeaderViewHolder -> holder.bindsHeader() is FooterViewHolder -> holder.bindsFooter() is StudentViewHolder -> holder.bindTo(getStudentItem(position)) } } private fun getStudentItem(position: Int): Student? { return getItem(position - 1) } override fun getItemCount(): Int { return super.getItemCount() + 2 } override fun registerAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) { super.registerAdapterDataObserver(AdapterDataObserverProxy(observer, 1)) } companion object { private val diffCallback = object : DiffUtil.ItemCallback<Student>() { override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean = oldItem == newItem } private const val ITEM_TYPE_HEADER = 99 private const val ITEM_TYPE_FOOTER = 100 } } 复制代码
若是你想查看运行完整的demo,这里是本文sample的地址:
文末最终的方案是否有更多优化的空间呢?固然,在实际的项目中,对其进行简单的封装是更有意义的(好比Builder
模式、封装一个Header
、Footer
甚至二者都有的装饰器类、或者其它...)。
本文旨在描述Paging使用过程当中 遇到的问题 和 解决问题的过程,所以项目级别的封装和实现细节不做为本文的主要内容;关于Header
和Footer
在Paging
中的实现方式,若是您有更好的解决方案,期待与您的共同探讨。
争取打造 Android Jetpack 讲解的最好的博客系列:
Android Jetpack 实战篇:
Hello,我是却把清梅嗅,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人我的博客或者Github。
若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?