Android复杂列表的实现

Android复杂列表的实现

RecyclerView控件从2014发布以来,目前已经普遍用于项目中,来承载各种列表内容。同时,列表样式也随着项目变的越来越复杂,从简单统一的列表,变化成头部、脚部、不同类型的Item互相组合。本文将通过一些开源库来学习一下如何实现各种复杂类型的列表,分析了viewType应该如何与视图、数据相绑定,并将业务逻辑单独分离。

初步实现

问题的开始是这样的:项目里有个页面,整个列表采用ListView实现,除了常规的列表项外,还有两个自定义的View也要随着页面滑动。Ok,listView支持addHead,而且还是多head,自定义view通过addHead方法添加到listview中,就一切ok。然而ListView毕竟渐渐过时了,打算采用RecyclerView来重构一下。虽然RecyclerView不支持addHead这种方法,但是可以通过getItemViewType方法来实现返回多种类型。


@Override
public int getItemViewType(int position) {
    switch (position) {
        case 0:
            return TYPE_HEAD1;
        case 1:
            return TYPE_HEAD2;
        case 2:
            return TYPE_ITEM;
        default:
            return TYPE_ITEM;
        }
    }

即根据业务需求,返回不同的类型的值,那么下一步,我们同时需要在onCreateViewHolder中针对不同的viewType来创建不同的ViewHolder,同样的,在onBindViewHolder中,也要处理不同的类型,特别的,如果不同类型的viewholder具有不同的方法的情况,还需要针对viewholder做一次类型转换。类似这样:


@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (getItemViewType(position) == TYPE_HEAD1) {
        ((Head1VH) holder).bindData();
    } else if (getItemViewType(position) == TYPE_HEAD2) {
        ((Head2VH) holder).bindData();
    } else if (getItemViewType(position) == TYPE_ITEM) {
        ((Item) holder).bindData();
    }
}

以上就是一般RecyclerView中实现多类型Item的方法,相应的变化一下,把头部和脚部当作特定类型的ItemType,并提供public方法共外部setHead即可支持添加头部。

问题进阶

上述的方法,是解决了特定业务情景下的问题,但是很明细不利于扩展和维护。首先,当列表除了头部外的部分依然会出现不同类型时,并且实际情况中,不同类型应该都是由服务器回传的数据来决定的,我们就不能在getItemViewType中简单的定义类型值来判断。 一个可能的做法是,在数据层里添加type字段,通过type字段来

@Override
public int getItemViewType(int position) {
    return datas.get(position).type;
}

然而在数据层包裹展示层需要的type字段并不是一个优雅的做法,它破坏了单一职责。同时,这么做也无法解决另一个问题:扩展性。 所谓扩展性就是Adapter最好能在数据类型变化时候,内部实现逻辑不需要改变,只是外部添加新的功能即可。那么这就要求Adapter对数据层是解耦的,不能显式的持有外部的数据。Adapter设计之初,是为了兼容千变万化的数据结构,并不是千变万化的类型结构,因此,应该考虑把不同类型的变化从Adapter内部隔离开。

                                           

       

GitHub上关于多类型Item的RecyclerView的实现有很多库,基本的思路是通过一个Manager类来管理多种类型中:数据和视图的对应关系。实际上,都是围绕如何解决viewType、数据、视图的对应关系来进行一系列的封装。 下面介绍两个实现的比较简洁而灵活的库:

AdapterDelegates的思路是使用自定义的Adapter来“hook”原来的RecyclerView的Adapter,主要的Adapter方法如onBindViewHolder和onCreateViewHolder方法都被劫持使用adpter内部的一个Manager类来实现,参看下面的类图会更加容易理解。

              

上图是这个库的基本类图,省略了两个非必要的类,其中只列出了一些典型的方法和对象。以onBindViewHolder()为例,可以看到从最顶层开始,这个方法会一步步往下调用,一直到AdapterDelegate这层,这一层也是最终面向使用者需要关心的层次,通过继承抽象类AdapterDelegate,实现其中的方法,来完成业务逻辑和UI表现,代码如下,和普通的RV.Adapter方法没有区别:

    @NonNull
    @Override
    protected NormalItemVH onCreateViewHolder(@NonNull ViewGroup parent) {
        return new NormalItemVH(inflater.inflate(R.layout.normal_item, parent, false));
    }

    @Override
    protected void onBindViewHolder(@NonNull NormalItem item, @NonNull final NormalItemVH viewHolder, @NonNull List<Object> payloads) {
        viewHolder.imageView.setImageResource(item.resId);
        viewHolder.imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                DetailsActivity.startActivity(view.getContext());
            }
        });
        viewHolder.textView.setText(item.content);
        viewHolder.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String old = viewHolder.textView.getText().toString();
                viewHolder.textView.setText(old + " " + (int) (10 * Math.random()));

            }
        });
    }

但是通过这一层的封装,成功的把多类型的情况分隔开,每种类型只需要在各种的AdapterDelegate中去编写业务逻辑就可以,Adapter中的职业就非常简单,只需要持有AdapterDelegateManager,由这个Manager类来维护每种类型具体对应的AdapterDelegate,而由AdapterDelegate维护UI和数据的绑定关系。

                             

如此,面对多类型的情况或者在已有的业务基础上增加了新的类型,都不再用去修改Adapter的基本实现,只要做两件事:

  • 编写类型的AdapterDelegate来实现UI展示、数据绑定、点击事件等工作
  • 通过AdapterDelegateManager注册新的AdapterDelegate

下面是一个demo例子(gif画质比较渣,将就着看。。)

                                                     

整个列表是一个RecyclerView,包含了两种不同类型的头部,简单的Item类型和可横向滑动展示的Item类型共计4种。来看看这个RecyclerView的Adapter实现:

    class ItemList2Adapter extends ListDelegationAdapter<List<Item>> {
        Activity activity;
        List<Item> datas;

        public ItemList2Adapter(Activity activity, List<Item> datas) {
            this.activity = activity;
            this.datas = datas;
            delegatesManager.addDelegate(new Head1Delegate(activity))
                    .addDelegate(new Head2Delegate(activity))
                    .addDelegate(new NormalDelegate(activity))
                    .addDelegate(new HorizontalItemDelegate(activity));
            setItems(datas);
        } 
    }

从代码里可以看到,整个Adapter是非常简洁和清晰的,业务逻辑归于Delegate当中解决,viewType和类型的映射关系放到delegateManager中处理。具体Delegate的代码就不贴了,和常规单类型Adapter的写法一致。下面再看看另一个库的思路:MuliTypeAdapter. 这里就不自己画类图了,从其作者的文档中引用一幅图,如下:

从上文所说的基本原则来分析,我们应重点关注其如何实现viewType字段和类型的映射,以及如何和RV.Adaper交互。从类名和继承关系来看,我们可以知道,MultiTypeAdapter应该是充当之前所说的Manage的角色,同时,这个类实现了两个接口:

  • TypePool
  • FlatTypeAdapter

因此,维护viewType和类型映射关系就必然会体现在其中。而类Items是一个继承ArrayList

    @Override
    public int getItemViewType(int position) {
        Object item = items.get(position);
        return indexOf(flattenClass(item));
    }


    @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int indexViewType) {
        if (inflater == null) {
            inflater = LayoutInflater.from(parent.getContext());
        }
        ItemViewProvider provider = getProviderByIndex(indexViewType);
        provider.adapter = MultiTypeAdapter.this;
        return provider.onCreateViewHolder(inflater, parent);
    }


    @SuppressWarnings("unchecked") @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Object item = items.get(position);
        ItemViewProvider provider = getProviderByClass(flattenClass(item));
        provider.onBindViewHolder(holder, flattenItem(item));
    }

从MuliTypeAdapter的几个重点方法可以看出,其调用的方法几乎都是接口或者抽象类的空方法,这侧面体现出来此库的高度可定制性,所有的方法实现都可以由具体的实现类来决定。

从getViewType方法中可以看到,其返回值由indexOf方法确定,而这个方法定义在TypePool接口中,由MultiTypePool实现,当然我们也可以自己实现然后替换掉。从MultiTypePool的源码中分析:

    private ArrayList<Class<?>> contents;
    private ArrayList<ItemViewProvider> providers;

    public void register(Class<?> clazz,ItemViewProvider provider) {
        if (!contents.contains(clazz)) {
            contents.add(clazz);
            providers.add(provider);
        } else {
            int index = contents.indexOf(clazz);
            providers.set(index, provider);
            Log.w(TAG, "You have registered the " + clazz.getSimpleName() + " type. " +
                "It will override the original provider.");
        }
    }

    @Override
    public int indexOf(Class<?> clazz) {
        int index = contents.indexOf(clazz);
        if (index >= 0) {
            return index;
        }
        for (int i = 0; i < contents.size(); i++) {
            if (contents.get(i).isAssignableFrom(clazz)) {
                return i;
            }
        }
        return index;
    }

可以看到,不同于AdapteDelegate中绑定viewType和Delegate,在这里,它将数据类Class和ItemViewProvider进行了绑定,分别用两个ArrayList来存储对象,用index索引作为viewType的值。如下图示意:

             

当Adapter中注册类型时,将两者绑定;getViewType时,则首先通过position拿到数据类型,再通过数据类型拿到对应的UI类型;onBindViewHolder时,同样通过position拿到数据类型,拿到ItemViewProvider,继而调用ItemViewProvider的onBindViewHolder方法去交由实现类处理。以上应该可以基本明白该库是如何维护viewType、数据类型和UI类型的映射关系的。

而在编写Adapter的过程中,特别是多类型的Adapter过程中,常常会发现自己不得不在onBindVieHolder方法中,对holder转型来调用其内部方法,或者对数据转型来使用其字段值,大量的类型转换既显得臃肿又影响速度。既然我们已经把不同类型的情况已经独立成一个个ItemViewProvider(或者AdapterDelegate,另一个库中的称呼),那么在相应的实现类中,我们也希望能正确的分发数据类型和视图类型。 在AdatperDelegates库中,如果我们的业务实现类直接继承与AdapterDelegate来编写,是这样的:

public class Head1Delegate extends AdapterDelegate<List<Item>> {
 ...

@Override
protected void onBindViewHolder(@NonNull List<Item> items, 
int position, @NonNull RecyclerView.ViewHolder holder, 
@NonNull List<Object> payloads) {

((Head1VH) holder).imageView.
setImageResource(((Head1) items.get(position)).getResId());
    }
}

可以看到还是没有避免类型转换。作者其实也意识到这点,因此提供了一个AbsListItemAdapterDelegate类来供我们继承,其内部通过泛型预先帮我们做好类型转换,再分发下去:

 public abstract class AbsListItemAdapterDelegate<I extends T, T, 
 VH extends RecyclerView.ViewHolder>
    extends AdapterDelegate<List<T>> {
    ...

@Override 
protected final void onBindViewHolder(@NonNull List<T> items, int position,
      @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {

    onBindViewHolder((I) items.get(position), (VH) holder, payloads);
  }

MuliTypeAdapter则干脆的多,在定义ItemViewProvider的抽象方法时就已经考虑了这个问题,解决方案和上述一致,但是写法上看起来更为优雅:

   protected abstract void onBindViewHolder(@NonNull V holder, @NonNull T t);

当然,这样做本质是在内层做好转型再分发,如果要真正意思上的避免转型,可以采用访问者模式(参见:Writing Better Adapter)

关于MuliTypeAdapter的Demo就不做了,其官方上例子已经很详尽。并且,除了之前提到的核心逻辑外,其还提供了全局类型池设计、数据二次分发设计(即没有讨论的FlatTypeAdapter接口),感兴趣的可以继续了解。

上述两个库,都做到了对不同类型Item的分离,每次组装一个列表时,只需要把数据源正确的组装好,adapter内部会通过各自实现的Manager来定位对应的UI来展示。在实际开发中,可能的问题或许是不同Item之间的关联性,比如一个头部类型的带有联动其他Item的交互的话,就需要打破这种独立性(此时需要通过构造函数等方法传入其他对象的实例)。另外,对于常见的头部、列表、脚部的需求来说,实际上在此都是当作三种类型来处理,那么对于服务器回传的列表数据,我们需要自行包裹上头部、脚部的数据类型,这样才能正确的被处理,也是相对麻烦之处。

参考文章:

本文来自网易实践者社区,经作者周龙授权发布。