Android官方架构组件Paging-Ex_为分页列表添加Header和Footer

正如我所标注的,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 {
// …
}

因此,我们需要这样实现:

// 这里我们只能指定Student类型
class SimpleAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) {
// …
}

有同学提出,我们可以将这里的Student指定为某个接口(比如定义一个ItemData接口),然后让StudentHeader对应的Model都去实现这个接口,然后这样:

class SimpleAdapter : PagedListAdapter<ItemData, RecyclerView.ViewHolder>(diffCallback) {
// …
}

看起来确实可行,但是我们忽略了一个问题,那就是本小节要阐述的:

我们并没有直接持有数据源

回到初衷,我们知道,Paging最大的亮点在于 自动分页加载,这是观察者模式的体现,配置完成后,我们并不关心 数据是如何被分页、何时被加载、如何被渲染 的,因此我们也不需要直接持有List<Student>(实际上也持有不了),更无从谈起手动为其添加HeaderItemFooterItem了。

以本文为例,实际上所有逻辑都交给了ViewModel

class CommonViewModel(app: Application) : AndroidViewModel(app) {

private val dao = StudentDb.get(app).studentDao()

fun getRefreshLiveData(): LiveData<PagedList> =
LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder()
.setPageSize(15) //配置分页加载的数量
.setInitialLoadSizeHint(40) //初始化加载的数量
.build()).build()
}

可以看到,我们并未直接持有List<Student>,因此list.add(headerItem)这种 持有并修改数据源 的方案几乎不可行(较真而言,其实是可行的,但是成本过高,本文不深入讨论)。

2.尝试直接实现列表

接下来我针对直接实现多类型列表进行尝试,我们先不讨论如何实现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也许展示并不那么清晰,简单总结下,问题有两个:

  • 1.在我们进行下拉刷新时,因为Header更应该是一个静态独立的组件,但实际上它也是列表的一部分,因此白光一闪,除了Student列表,Header作为Item也进行了刷新,这与我们的预期不符;
  • 2.下拉刷新之后,列表 并未展示在最顶部,而是滑动到了一个奇怪的位置。

导致这两个问题的根本原因仍然是Paging计算列表的position时出现的问题:

对于问题1,Paging对于列表的刷新理解为 所有Item的刷新,因此同样作为ItemHeader也无法避免被刷新;

问题2依然也是这个问题导致的,在Paging获取到第一页数据时(假设第一页数据只有10条),Paging会命令更新position in 0..9Item,而实际上因为Header的关系,我们是期望它能够更新第position in 1..10Item,最终导致了刷新以及对新数据的展示出现了问题。

3.向Google和Github寻求答案

正如标题而言,我尝试求助于GoogleGithub,最终找到了这个链接:

PagingWithNetworkSample - PagedList RecyclerView scroll bug

如果您简单研究过PagedListAdapter的源码的话,您应该了解,PagedListAdapter内部定义了一个AsyncPagedListDiffer,用于对列表数据的加载和展示,PagedListAdapter更像是一个空壳,所有分页相关的逻辑实际都 委托 给了AsyncPagedListDiffer:

public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter {

final AsyncPagedListDiffer mDiffer;

public void submitList(@Nullable PagedList pagedList) {
mDiffer.submitList(pagedList);
}

protected T getItem(int position) {
return mDiffer.getItem(position);
}

public int getItemCount() {
return mDiffer.getItemCount();
}

public PagedList getCurrentList() {
return mDiffer.getCurrentList();
}
}

虽然Paging中数据的获取和展示我们是无法控制的,但我们可以尝试 瞒过 PagedListAdapter,即使Paging得到了position in 0..9List<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(listUpdateCallback,
AsyncDifferConfig.Builder(POST_COMPARATOR).build())

// 将所有方法重写,并委托给新的differ去处理
override fun getItem(position: Int): Any? {
return differ.getItem(position - 1)
}

// 将所有方法重写,并委托给新的differ去处理
override fun submitList(pagedList: PagedList?) {
differ.submitList(pagedList)
}

// 将所有方法重写,并委托给新的differ去处理
override fun getCurrentList(): PagedList? {
return differ.currentList
}
}

现在我们成功实现了上文中我们的思路,一图胜千言:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.另外一种实现方式

上一小节的实现方案是完全可行的,但我个人认为美中不足的是,这种方案 对既有的Adapter中代码改动过大

我新建了一个AdapterListUpdateCallback、一个ListUpdateCallback以及一个新的AsyncPagedListDiffer,并重写了太多的PagedListAdapter的方法——我添加了数十行分页相关的代码,但这些代码和正常的列表展示并没有直接的关系。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

总结

最后小编想说:不论以后选择什么方向发展,目前重要的是把Android方面的技术学好,毕竟其实对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

这里附上我整理的几十套腾讯、字节跳动,京东,小米,头条、阿里、美团等公司19年的Android面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

技术进阶之路很漫长,一起共勉吧~

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

技术进阶之路很漫长,一起共勉吧~

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-kW8B8VC5-1712690677159)]

  • 20
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值