Android 对 Adapter 的 ItemType 进行封装简化

前言

之所以要把 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>

代码详见:https://github.com/neverwoodsS/zy-open

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容