RecyclerView实现多类型Item布局--AdapterDelegates代码分析

内容简介

本文针对业务需求,对开源项目AdapterDelegates进行了简单的分析。感谢项目作者 Hannes Dorfmann的开源贡献。

需求分析

有类似微信卡包这样一个列表页面,要如何去实现。

乍一看,这是一个典型的列表类型的页面。有两个大的分类,会员卡和优惠券。简单的处理下,标题+列表,如此两个来实现。然而如果哪天说需要加个证件/银行卡什么的子项,在手动去布局明显有不少重复内容的东西总是有些难受的;那么,用一个标题+子列表作为一个item,这样做一个列表就可以了。但是列表中套着个子列表,item布局和数据设计复杂度略高。

真实的需求中看起来类似的item,总会存在形形色色的不一致之处,如果强行合在一个布局中实现,无论是布局文件还是对应的数据类设计都会比较复杂,可以预见的是实现上必然存在各种各样的条件分支。并且基于这种实现的可拓展性也比较差,再加入一种比较个性的item,那么布局、绘制、数据类都要加入各种各样的判断,并且冗杂在一起。

假设只有一个这样的页面需求,时间又很赶的情况下,或许可以苟且一下,然而当如此这般的页面需求一下出现六七个的时候,就要慎重考虑了。

其实只需要一个列表

实际上 RecyclerView.Adapter 中似乎已经考虑到这种情况的存在。之前使用的 RecyclerView 都比较简单,我们仔细观察 RecyclerView.Adapter 这个十分熟悉的建立
ViewHolder的抽象方法:

/**
 * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent
 * an item.
 * <p>
 * This new ViewHolder should be constructed with a new View that can represent the items
 * of the given type. You can either create a new View manually or inflate it from an XML
 * layout file.
 * <p>
 * The new ViewHolder will be used to display items of the adapter using
 * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display
 * different items in the data set, it is a good idea to cache references to sub views of
 * the View to avoid unnecessary {@link View#findViewById(int)} calls.
 *
 * @param parent The ViewGroup into which the new View will be added after it is bound to
 *               an adapter position.
 * @param viewType The view type of the new View.
 *
 * @return A new ViewHolder that holds a View of the given view type.
 * @see #getItemViewType(int)
 * @see #onBindViewHolder(ViewHolder, int)
 */
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);

有一个常被忽略的参数 —— viewType。同样的,从源码中也能找到一个默认的实现方法:

/**
 * Return the view type of the item at <code>position</code> for the purposes
 * of view recycling.
 *
 * <p>The default implementation of this method returns 0, making the assumption of
 * a single view type for the adapter. Unlike ListView adapters, types need not
 * be contiguous. Consider using id resources to uniquely identify item view types.
 *
 * @param position position to query
 * @return integer value identifying the type of the view needed to represent the item at
 *                 <code>position</code>. Type codes need not be contiguous.
 */
public int getItemViewType(int position) {
    return 0;
}

从注释和之前粗浅的使用 RecyclerView 的经验可以了解到,实现item不同布局应当是可行的。根据刷新接口的定义:

public abstract void onBindViewHolder(VH holder, int position);

首先通过 getItemViewType 接口,在 onCreateViewHolder 中根据不同的类型建立不同的ViewHolder,然后通过一定的映射关系,在 onBindViewHolder 中可以找到正确的数据来对不同的 ViewHolder 进行刷新。

那么现在可以确认,一个 RecyclerView 就可以实现各种各样的item的交互页面,那么上面所提到的需求,其实页面上只用一个RecyclerView就好了,去掉标题类别、会员卡、优惠券之间指示与从属归类的概念关系,每个条目都是一个item,高度相似的布局可以归为一类实现即可。

所以新的问题就是如何设计这个Adapter,才能够尽量减少if语句的存在,做到代码的可扩展性和可维护性更加合理。如此通用的一个需求,GitHub总能给你一个很好的答案,AdapterDelegates 。它的DEMO效果图如下:

效果上完全符合需要,剩下的就是分析一下它的实现原理,考察一下可用性如何了。

关于这个库的用法与实现

从实现上考虑 RecyclerView.Adapter 大概要考虑以下几点:

  • item 布局与刷新
  • 对应的数据结构组织
  • item 中各个子控件的交互响应回调

最后一项可以先不考虑,这个粗糙一点还是精细一点完全根据业务实际情况来。理论上针对if条件分支的消除,就是通过一定的设计模式,利用面向对象语言接口、抽象类的特性,本质上使用继承、组合的手段来做,解除掉耦合。能够达到的理想状态是,要更改一个功能时,修改一个类即可;添加一个功能时,增加个别新类即可。

DEMO的分析

我们来看DEMO的实现方式,堆一个类图出来:

红色部分是DEMO实现类,其余部分是库中的工具类。先主要分析红色部分,作为一个库的用户要实现这样一个多样化的列表,都需要实现哪些类。

  • ReptilesAdapter,RecyclerView.Adapter的实现类,它的复杂程度是我们的关注点;
  • 以Delegate为类名结尾的这些兄弟类,主要就是负责不同item的布局与刷新实现了。顾名思义,“Delegate”就是代理,对于这一部分,我们重点来看它们与Adapter之间的调用关系、或者说继承组合关系,其次就是跟数据类的对应的刷新实现;
  • DisplayableItem 向下的一众接口实现子类,这部分属于数据类,这里需要注意的是具体的数据类与绘制部分的映射是如何实现的。

ReptilesAdapter

该类源码不到二十行,至少它很简洁。

public class ReptilesAdapter extends ListDelegationAdapter<List<DisplayableItem>> {

  public ReptilesAdapter(Activity activity, List<DisplayableItem> items) {

    // Delegates
    this.delegatesManager.addDelegate(new GeckoAdapterDelegate(activity));
    this.delegatesManager.addDelegate(new SnakeListItemAdapterDelegate(activity));
    this.delegatesManager.setFallbackDelegate(new ReptilesFallbackDelegate(activity));

    setItems(items);
  }
}
  • 是库类ListDelegationAdapter的子类;
  • 只有一个构造函数,需要传入数据列表,DisplayableItem接口的一个列表;
  • 构造函数中,对于delegatesManager——显然是前辈类的一个AdapterDelegatesManager实体,相当于写死增加了两个个代理Delegate和一个默认Delegate。

Delegates

这部分兄弟类实现上都是相似的,这里只选取GeckoAdapterDelegate看看,也不长六十余行,这里截取重要的代码片段。

public class GeckoAdapterDelegate extends AdapterDelegate<List<DisplayableItem>> {
    //...
    @Override
    public boolean isForViewType(@NonNull List<DisplayableItem> items, int position) {
        //类型判断 由位置对应的数据类型判断
        return items.get(position) instanceof Gecko;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
        //独立的view生成
        return new GeckoViewHolder(inflater.inflate(R.layout.item_gecko, parent, false));
    }

    @Override
    public void onBindViewHolder(@NonNull List<DisplayableItem> items, int position,
                                 @NonNull RecyclerView.ViewHolder holder, @Nullable List<Object> payloads) {
        GeckoViewHolder vh = (GeckoViewHolder) holder;
        Gecko gecko = (Gecko) items.get(position);
        //数据类型转换 view刷新
        vh.name.setText(gecko.getName());
        vh.race.setText(gecko.getRace());
    }
    //对应的ViewHolder
    static class GeckoViewHolder extends RecyclerView.ViewHolder {

        public TextView name;
        public TextView race;
         //...
    }
}
  • 作为一个代理类,那么它拥有跟 RecyclerView.Adapter 长的十分相像的接口也是意料之中了;
  • onBindViewHolder,很明显本类对应的数据实体类是 Gecko ,具体的刷新实现采用了强制类型转换的方法;
  • isForViewType ,判断当前item与ViewHolder的类型对应关系,如果仔细读过前面挂出的注释的话,猜也可以猜到是对getItemViewType接口的一个支持接口,数据类型符合——instanceof,来完成映射。

诸多item

从类图就能看出来,数据类比较纯粹,是一个简单的 extneds/implement 关系。最上层的是一个空接口,作为一个抽象的存在放到抽象关系的AbsDelegationAdapter占好位置。最终的数据使用是具体到Delegate的实体类中,利用类型强转,拿到真实的数据来刷新页面的。

使用者说

从功能上看,这是在一个列表中以动物为主题显示不同的item分隔的实现。从开源库的DEMO——使用者的角度而言,目前有Gecko、Snake两种动物的布局item,假如要增加一个Dragon,需要如何做呢?从基本类图和上面的简单分析可以得出:

  • 建立一个Dragon数据类,让它继承于Animal,加入特有的属性;
  • 建立一个DragonAdapterDelegate代理类,继承 AdapterDelegate 类,定义好业务需求的ViewHolder,复写上面挂出的关键方法;
  • 在 ReptilesAdapter 的构造函数中把 DragonAdapterDelegate 加入到代理管理器中:

    this.delegatesManager.addDelegate(new DragonAdapterDelegate(activity));
    

跟把大象装进冰箱差不多,三步之后就可以实现了。而且完全解耦,基本上通过新增类的方式解决掉了新增需求,简洁优雅。

再进一步,库源码

实际上DEMO的类图已经是全工程的一个简图了,现在看黑色的部分——库中的类,忽略掉边边角角的子类,会发现核心类还是在于AbsDelegationAdapter。作为RecyclerView.Adapter的继承者,既用来持有列表数据,还持有代理管理类AdapterDelegatesManager。不难看出,基于RecyclerView.Adapter的内部调用逻辑,该类利用AdapterDelegatesManager做了一个主要抽象函数的代理调用,从而高效的实现了多类型view的需求。把关键的属性和方法加上,画一个库的类图,就更加清晰明了了:

对于的源码,就贴一个 getItemViewType 函数:

/**
* Must be called from {@link RecyclerView.Adapter#getItemViewType(int)}. Internally it scans all
* the registered {@link AdapterDelegate} and picks the right one to return the ViewType integer.
*
* @param items Adapter's data source
* @param position the position in adapters data source
* @return the ViewType (integer). Returns {@link #FALLBACK_DELEGATE_VIEW_TYPE} in case that the
* fallback adapter delegate should be used
* @throws NullPointerException if no {@link AdapterDelegate} has been found that is
* responsible for the given data element in data set (No {@link AdapterDelegate} for the given
* ViewType)
* @throws NullPointerException if items is null
*/
public int getItemViewType(@NonNull T items, int position) {

    if (items == null) {
        throw new NullPointerException("Items datasource is null!");
    }

    int delegatesCount = delegates.size();
    for (int i = 0; i < delegatesCount; i++) {
        AdapterDelegate<T> delegate = delegates.valueAt(i);
        if (delegate.isForViewType(items, position)) {
            return delegates.keyAt(i);
        }
    }

    if (fallbackDelegate != null) {
        return FALLBACK_DELEGATE_VIEW_TYPE;
    }

    throw new NullPointerException(
    "No AdapterDelegate added that matches position=" + position + " in data source");
}

getItemViewType 本身在 RecyclerView.Adapter 的调用流程中就是发生在onCreateViewHolder 调用之前,本类覆盖默认的恒返回0的父类函数,通过代理的isForViewType 函数,与对应的数据列表来确认分发给 onCreateViewHolder 的类型,最终实现多类型item列表的功能,与之前对代理函数 isForViewType 的猜测相符。

简要的总结

  • 论设计模式的重要性,针对业务需要选择合适的设计模式很重要,选对了思路对后面的拓展开发、业务维护来说事半功倍。对于这次的业务需求而言,使用这个开源库,实现好一个基础的页面模型之后,其余的五六个页面的工作基本只需要考虑数据部分设计、代理部分布局刷新与子控件交互传递的问题。写好了,往框架里塞即可,非常简单。
  • 嫁接还是需要功力的,AdapterDelegates 的实现首先是基于对 RecyclerView 整个框架体系的理解。我们常说自己新写一套代码容易、维护别人的代码比较困难,那么给予别人的代码之上开发一套使原功能更加易用的东西想必要求更高。实际上AdapterDelegates中也同时代理了Adapter中关于view管理部分的接口,也考虑了一些默认情况的处理,整体上是比较完善的。如我等只是简单使用 RecyclerView 的人,不到需求烧到眉毛是不会思考viewType这个参数到底用处在哪里的,也就无从有思考RecyclerView内部原理和二次开发的动力,这一点需要反思。
  • 类图有助于理解设计。
  • 无论是官方代码,还是AdapterDelegates,注释风格十分专业详细,在SDK的开发过程中是可以借鉴的。
感谢您赏个荷包蛋~