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方法没有区别:
public class NormalDelegate extends AbsListItemAdapterDelegate<NormalItem, Item, NormalDelegate.NormalItemVH> {
@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<Object>的空类,表明了这个类将是所有数据结构的基类。最后,唯一单独没有联系的ItemViewProvider<C,V>则可以推断为用来实现业务逻辑和UI展示。如此,基本要素都一一对应上,接下来看看它是如何实现其中的功能。
public class MultiTypeAdapter extends RecyclerView.Adapter<ViewHolder>{
@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的交互的话,就需要打破这种独立性(此时需要通过构造函数等方法传入其他对象的实例)。另外,对于常见的头部、列表、脚部的需求来说,实际上在此都是当作三种类型来处理,那么对于服务器回传的列表数据,我们需要自行包裹上头部、脚部的数据类型,这样才能正确的被处理,也是相对麻烦之处。
参考文章: