前言
之所以要把 ItemType 的封装单独拉出一篇文章,是因为前面两篇分别是针对 ListAdapter 和 RecyclerAdapter,而 ItemType 封装的思路则都是一样的,基本上没有区别可言。另外一方面,我也觉得一篇文章的篇幅还是应该有点限制,不然我自己看着都没耐心。
首先我觉得应该感谢一下鸿扬大神,很大程度上我对 ItemType 的处理参考了他的 baseAdapter 项目,虽然我仍然坚持我对某一个细节的处理,但是他的项目确实给了我不少灵感和参考。后面也会针对这一点进行对比。
正文
在上一期的 RecyclerAdapter 的基础上,做最小修改实现 ItemType 的代码大概会是这样的,也是我最早实现的方法
adapter = new RecyclerAdapter<Item>(this, dataSource) {
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == 0) {
return new RecyclerViewHolder(mInflater.inflate(layout0, parent, false));
} else {
return new RecyclerViewHolder(mInflater.inflate(layout1, parent, false));
}
}
@Override
protected void onBindData(RecyclerViewHolder holder, Item data, int position) {
if (getItemViewType(position) == 0) {
//TODO
} else {
//TODO
}
}
@Override
protected int getItemViewType(int position, Item data) {
return data.type;
}
};
可以看出,在不对 RecyclerAdapter 大改的条件下, 至少需要重写 onCreateViewHolder、onBindData、getItemViewType 三个方法,而且这还是我已经对 RecyclerAdapter 做出了一定程度的修改的情况:
1.构造器重载,可以不传入布局 id
2.添加getItemViewType(int, Item) 方法,避免手动写 dataSource.get(position) 这样的意大利面代码
我还尝试过传入一个 Map<type, layoutRes>,以减少重写 onCreateViewHolder 这一重复工作。事实上,我对 ListAdapter 的 ItemType 目前就是这样子做的,最终写出来的代码大概是这样的:
class MultiTypeAdapter extends ListAdapter<Item> {
public MultiTypeAdapter(Context context, List<Item> data, Map<Integer, Integer> layouts) {
super(context, data, layouts);
}
@Override
protected void setItem(View convertView, Item data, int position) {
if (getItemViewType(position) == Item.TYPE_ONE) {
setTypeOneItem(convertView, data, position);
} else {
setTypeTwoItem(convertView, data, position);
}
}
private void setTypeOneItem(View convertView, Item data, int position) {
//TODO
}
private void setTypeTwoItem(View convertView, Item data, int position) {
//TODO
}
@Override
protected int getItemViewType(int position, Item data) {
return data.type;
}
}
Map<Integer, Integer> layouts = new HashMap<>();
layouts.put(Item.TYPE_ONE, R.layout.item_multi_type_one);
layouts.put(Item.TYPE_TWO, R.layout.item_multi_type_two);
adapter = new MultiTypeAdapter(this, dataSource, layouts);
从代码可以看出,确实减少了我们去处理什么 type 加载什么布局的工作,但是无可避免的在绑定的时候却不得不再去判断一次 type,然后做不同的事情。说好听一点,这并不符合我们期望的绑定方法只做绑定的事情,也担起了分类的责任;说简单一点,那就是这代码不优雅~
思考
在不同的 Activity 中,长得一样、逻辑也差不多的 ListView、RecyclerView 可以使用同一个 Adapter 就完成 Item 的展示。但是考虑到 ItemType 的话,事情就会复杂些许。例如:我有一个拥有类型1和类型2的 ListView 取名为 A,我有一个拥有类型2和类型3的 ListView 取名为 B,如果按照上面的做法,那么我只能写两个 Adapter 。如果把情况考虑复杂一点,A 对应123,B 对应234,那么 A 和 B 他们有一大半的 Item 类型是一致的,这意味着我们的重复代码会很多。再考虑极端一些呢?……
可是,A 和 B 的不同导致他们几乎不得不使用不同的 Adapter,那我们应该怎么办呢?
我个人有一个观点:<b>对 Adapter 的复用实质上是为了对 Item 的复用</b>,这里的 Item 包括布局、数据绑定、事件监听等。既然 Adapter 一定是不同的,为了实现 Item 的复用,我们是不是应该考虑把布局、数据绑定、事件监听等处理从 Adapter 中剥离出来?
于是 Delegate 类的价值就产生出来了,我们先看看鸿扬大神的代码:
/**
* Created by zhy on 16/6/22.
*/
public interface ItemViewDelegate<T>
{
int getItemViewLayoutId();
boolean isForViewType(T item, int position);
void convert(ViewHolder holder, T t, int position);
}
三个方法依次为:
1.向 Adapter 提供布局文件的 id
2.判断传入的 item 是不是自己应该处理的类型
3.绑定 holder 和数据
忽略掉 Adapter 的具体封装,使用更是简单无比
MultiItemTypeAdapter adapter = new MultiItemTypeAdapter(this,mDatas);
adapter.addItemViewDelegate(new MsgSendItemDelagate());
adapter.addItemViewDelegate(new MsgComingItemDelagate());
每种Item类型对应一个ItemViewDelegete,例如
public class MsgComingItemDelagate implements ItemViewDelegate<ChatMessage>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.main_chat_from_msg;
}
@Override
public boolean isForViewType(ChatMessage item, int position)
{
return item.isComMeg();
}
@Override
public void convert(ViewHolder holder, ChatMessage chatMessage, int position)
{
holder.setText(R.id.chat_from_content, chatMessage.getContent());
holder.setText(R.id.chat_from_name, chatMessage.getName());
holder.setImageResource(R.id.chat_from_icon, chatMessage.getIcon());
}
}
鸿扬大神的思路,大致上是在 adapter 调用 getItemType 的时候,遍历所有 delegate,调用 delegate 的 isForViewType 来判断是否是自己的类型,如果是的话就停止遍历,返回这个 delegate。
public int getItemViewType(T item, int position)
{
int delegatesCount = delegates.size();
for (int i = delegatesCount - 1; i >= 0; i--)
{
ItemViewDelegate<T> delegate = delegates.valueAt(i);
if (delegate.isForViewType( item, position))
{
return delegates.keyAt(i);
}
}
throw new IllegalArgumentException(
"No ItemViewDelegate added that matches position=" + position + " in data source");
}
有了符合条件的 delegate,就可以将渲染 item 的任务交给它了。于是就有了上面三行代码实现一个多类型的 Adapter 如此精简的代码。
代码详情请至鸿扬大神的 github 查看:
https://github.com/hongyangAndroid/baseAdapter
真·正文
请原谅我鸡蛋里面挑骨头。
虽然鸿扬大神提供的方案可以说是精简到了极致,但是在复用方面我个人是持怀疑态度的。鸿扬大神的 Delegate 是自己决定我是不是属于这个类型。回到上面 A、B 两个 ListView 的例子中,我们能保证 A、B 中使用类型2和3的条件是一样的吗?不能,所以我们只能去修改 isForViewType 方法来兼容两种条件;而当这两种条件之间无法兼容的时候,我们只能让 A、B 中的同一种类型使用不同的 Delegate 类,即便他们长得一样,交互也一样。
即:<b>当业务发生变化的时候</b>,我可能会需要去修改 Delegate 类,或者增加仅有 isForViewType 实现不同的类。在比较苛刻的条件下,这并没有真正的做到 Item 复用。
而我所期望的 Delegate,什么时候用它,什么条件下用它,这不应该由它自己去决定,因为 Delegate 并不懂业务,我也不希望它和业务耦合在一起,我只是希望它能够根据传入的数据对象执行绑定工作而已。
所以我的 Delegate 是这样的:
public interface AdapterDelegate<T> {
int getLayoutId();
void bind(RecyclerViewHolder holder, T data, int position);
}
因为 delegate 不负责类型的判断,所以使用时稍微复杂一些:
adapter = new MultiTypeRecyclerAdapter<MultiTypeItem>(this, dataSource) {
@Override
protected int getItemViewType(MultiTypeItem data) {
return data.type;
}
};
adapter.addDelegate(MultiTypeItem.TYPE_ONE, new TypeOneDelegate())
.addDelegate(MultiTypeItem.TYPE_TWO, new TypeTwoDelegate());
首先,adapter 添加 delegate 的时候是以键值对的形式添加的,可以指定 delegate 去处理哪一种类型;
其次,adapter 需要重写一个 getItemViewType 方法,告诉 adapter 判断类型的依据。
这样子, adapter 就知道了什么时候去使用哪个 delegate。而当业务发生变化,但是 UI 没改的情况下,我不需要改动任何一个 delegate,而是改 adapter 定义的代码。
附上 MultiTypeRecyclerAdapter 的代码:
public class MultiTypeRecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerViewHolder> {
protected Context mContext;
protected List<T> mData;
protected LayoutInflater mInflater;
protected SparseArray<AdapterDelegate<T>> delegates = new SparseArray<>();
protected int layoutRes;
public MultiTypeRecyclerAdapter(Context context) {
this.mData = new ArrayList<>();
this.mContext = context;
this.mInflater = LayoutInflater.from(mContext);
}
public MultiTypeRecyclerAdapter(Context context, List<T> data) {
this.mData = data;
this.mContext = context;
this.mInflater = LayoutInflater.from(mContext);
}
public MultiTypeRecyclerAdapter(Context context, List<T> data, int layoutRes) {
this.mData = data;
this.mContext = context;
this.mInflater = LayoutInflater.from(mContext);
this.layoutRes = layoutRes;
}
public MultiTypeRecyclerAdapter(Context context, List<T> data, AdapterDelegate<T> delegate) {
this.mData = data;
this.mContext = context;
this.mInflater = LayoutInflater.from(mContext);
delegates.put(0, delegate);
}
public MultiTypeRecyclerAdapter<T> addDelegate(int type, AdapterDelegate<T> delegate) {
delegates.put(type, delegate);
return this;
}
public MultiTypeRecyclerAdapter<T> addDelegate(AdapterDelegate<T> delegate) {
return addDelegate(0, delegate);
}
public void refresh(List<T> data) {
try {
this.mData = data;
notifyDataSetChanged();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (delegates.size() == 0) {
return new RecyclerViewHolder(mInflater.inflate(layoutRes,
parent,
false));
} else {
return new RecyclerViewHolder(mInflater.inflate(delegates.get(viewType).getLayoutId(),
parent,
false));
}
}
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position) {
if (delegates.size() == 0) {
bind(holder, mData.get(position), position);
} else {
AdapterDelegate<T> delegate = delegates.get(getItemViewType(position));
delegate.bind(holder, mData.get(position), position);
}
}
@Override
public int getItemCount() {
return mData.size();
}
@Override
public int getItemViewType(int position) {
return getItemViewType(mData.get(position));
}
/**
* 由子类处理,默认返回 0
* @param data
* @return
*/
protected int getItemViewType(T data) {
return 0;
}
/**
* 单类型时子类需实现的方法
* 处理绑定 view
* @param holder
* @param data
* @param position
*/
protected void bind(RecyclerViewHolder holder, T data, int position) {
}
}
<b>最后,再次感谢鸿扬大神,他的 baseAdapter 项目更为成熟,包含的功能也更多,让我受益匪浅。</b>