Android 對 Adapter 的 ItemType 進行封裝簡化

前言

之所以要把 ItemType 的封裝單獨拉出一篇文章,是因為前面兩篇分別是針對 ListAdapter 和 RecyclerAdapter,而 ItemType 封裝的思路則都是一樣的,基本上沒有區(qū)別可言。另外一方面,我也覺得一篇文章的篇幅還是應該有點限制,不然我自己看著都沒耐心。

首先我覺得應該感謝一下鴻揚大神,很大程度上我對 ItemType 的處理參考了他的 baseAdapter 項目,雖然我仍然堅持我對某一個細節(jié)的處理,但是他的項目確實給了我不少靈感和參考。后面也會針對這一點進行對比。

正文

在上一期的 RecyclerAdapter 的基礎(chǔ)上,做最小修改實現(xiàn) ItemType 的代碼大概會是這樣的,也是我最早實現(xiàn)的方法

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 三個方法,而且這還是我已經(jīng)對 RecyclerAdapter 做出了一定程度的修改的情況:

1.構(gòu)造器重載,可以不傳入布局 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);

從代碼可以看出,確實減少了我們?nèi)ヌ幚硎裁?type 加載什么布局的工作,但是無可避免的在綁定的時候卻不得不再去判斷一次 type,然后做不同的事情。說好聽一點,這并不符合我們期望的綁定方法只做綁定的事情,也擔起了分類的責任;說簡單一點,那就是這代碼不優(yōu)雅~

思考

在不同的 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 的復用實質(zhì)上是為了對 Item 的復用</b>,這里的 Item 包括布局、數(shù)據(jù)綁定、事件監(jiān)聽等。既然 Adapter 一定是不同的,為了實現(xiàn) Item 的復用,我們是不是應該考慮把布局、數(shù)據(jù)綁定、事件監(jiān)聽等處理從 Adapter 中剝離出來?

于是 Delegate 類的價值就產(chǎn)生出來了,我們先看看鴻揚大神的代碼:

/**
 * 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 和數(shù)據(jù)

忽略掉 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 調(diào)用 getItemType 的時候,遍歷所有 delegate,調(diào)用 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 的任務交給它了。于是就有了上面三行代碼實現(xiàn)一個多類型的 Adapter 如此精簡的代碼。
代碼詳情請至鴻揚大神的 github 查看:
https://github.com/hongyangAndroid/baseAdapter

真·正文

請原諒我雞蛋里面挑骨頭。

雖然鴻揚大神提供的方案可以說是精簡到了極致,但是在復用方面我個人是持懷疑態(tài)度的。鴻揚大神的 Delegate 是自己決定我是不是屬于這個類型?;氐缴厦?A、B 兩個 ListView 的例子中,我們能保證 A、B 中使用類型2和3的條件是一樣的嗎?不能,所以我們只能去修改 isForViewType 方法來兼容兩種條件;而當這兩種條件之間無法兼容的時候,我們只能讓 A、B 中的同一種類型使用不同的 Delegate 類,即便他們長得一樣,交互也一樣。

即:<b>當業(yè)務發(fā)生變化的時候</b>,我可能會需要去修改 Delegate 類,或者增加僅有 isForViewType 實現(xiàn)不同的類。在比較苛刻的條件下,這并沒有真正的做到 Item 復用。

而我所期望的 Delegate,什么時候用它,什么條件下用它,這不應該由它自己去決定,因為 Delegate 并不懂業(yè)務,我也不希望它和業(yè)務耦合在一起,我只是希望它能夠根據(jù)傳入的數(shù)據(jù)對象執(zhí)行綁定工作而已。

所以我的 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 判斷類型的依據(jù)。

這樣子, adapter 就知道了什么時候去使用哪個 delegate。而當業(yè)務發(fā)生變化,但是 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;
    }

    /**
     * 單類型時子類需實現(xiàn)的方法
     * 處理綁定 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容