Android official architectural components Paging-Ex: Add Header and Footer tab to list

This article has been authorized "Jade just said" micro-channel public number exclusive release

2019/12/24 supplement

One year from the date of publication, the author believes that this article should not be used as introductory tutorial blog series, on the contrary, the reader really want to understand the use Paging, you should first try to understand the paging component of the essential idea:

Reflection | Android Design and Implementation of a list of paging component Paging: System Overview
Reflection | Android Design and Implementation of a list of paging component Paging: architecture design and principles resolved

More than two articles will Paging paging component with a systematic overview, I strongly recommend that readers will learn more than two articles as read Paging priority highest learning materials, the priority should be on all other Paging Chinese blog reading .

This article and related extended reading:

Android official architectural components Paging: Paging library design aesthetic
Android official architectural components Paging-Ex: Add Header and Footer tab for the list of
Android Paging-Ex official architectural components: a list of state response management

Outline

PagingIt is Googlethe 2018 I / O conference launched applies to AndroidNative Development paging library, if you are not well aware of this official Imperial paging infrastructure components, welcome to refer to the author of this article:

Android official architectural components Paging: Paging library design aesthetics

I have been used in the actual project Pagingmore than six months, and the market compared to other popular paging library, Pagingthe biggest bright spot in its list of logical page is loaded into the package as a callback function DataSourcein the developer after the configuration is complete without paging control loaded, the list will automatically load the next page and display data.

This article explains: In order to use the Pagingadd list Headerand Footerthe whole process, some obstacles encountered in this process, and how they are addressing these obstacles - if you want to directly browse the ultimate solution, please read this article direct final solutions section.

Initial ideas

To RecyclerViewadd a list Headeror Footeris not a very troublesome thing, the easiest way is rough RecyclerViewand Headerjointly placed with a ScrollViewchild Viewbut it is no different to RecyclerViewturn a blind eye multiplexing mechanism itself, so this solution is not preferred.

A more suitable solution is achieved through a multi-type list (MultiType), in addition to its own list of Itemtypes other than, Headeror Footercan also be regarded as a Item, on the way to achieve this are already many online articles to explain, this article does not go into details.

Before the official start of this article, we take a look at the final results achieved, we as a Studentadded a list of paging Headerand Footer:

实现这种效果,笔者最初的思路也是通过 多类型列表 实现HeaderFooter,但是很快我们就遇到了第一个问题,那就是 我们并没有直接持有数据源

1.数据源问题

对于常规的多类型列表而言,我们可以轻易的持有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接口),然后让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<Student>> =
            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<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..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<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
    }
}

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

4.另外一种实现方式

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

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

当然,我可以将这些逻辑都抽出来放在一个新的类里面,但我还是感觉我 好像是模仿并重写了一个新的PagedListAdapter类一样,那么是否还有其它的思路呢?

最终我找到了这篇文章:

Android RecyclerView + Paging Library 添加头部刷新会自动滚动的问题分析及解决

这篇文章中的作者通过细致分析Paging的源码,得出了一个更简单实现Header的方案,有兴趣的同学可以点进去查看,这里简单阐述其原理:

通过查看源码,以添加分页为例,Paging对拿到最新的数据后,对列表的更新实际是调用了RecyclerView.AdapternotifyItemRangeInserted()方法,而我们可以通过重写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.AdapternotifyItemRangeInserted()方法去执行的——两者本质上并无区别

5.最终的解决方案

虽然上文只阐述了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的地址:

https://github.com/qingmei2/SamplePaging

6.更多优化点?

文末最终的方案是否有更多优化的空间呢?当然,在实际的项目中,对其进行简单的封装是更有意义的(比如Builder模式、封装一个HeaderFooter甚至两者都有的装饰器类、或者其它…)。

本文旨在描述Paging使用过程中 遇到的问题解决问题的过程,因此项目级别的封装和实现细节不作为本文的主要内容;关于HeaderFooterPaging中的实现方式,如果您有更好的解决方案,期待与您的共同探讨。

参考&感谢


系列文章

争取打造 Android Jetpack 讲解的最好的博客系列

Android Jetpack 实战篇


关于我

Hello,我是却把清梅嗅,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的个人博客或者Github

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

发布了99 篇原创文章 · 获赞 396 · 访问量 47万+

Guess you like

Origin blog.csdn.net/mq2553299/article/details/89075709