RecyclerView 之 Adapter 的簡化過程淺析

依然範特稀西發表於2017-02-17

前言

前面一篇文章介紹了對於RecyclerView 的擴充套件和封裝的一個庫,幫助我們在開發中可以快速新增一個列表,提高開發效率。還沒有看過的同學可以在看完本篇文章之後移步前一篇文章RecyclerView Adapter 優雅封裝,一個Adapter搞定所有列表,原始碼請看GithubCustomAdapter。本篇文章講的就是Adapter 簡化的思路和過程。

背景

Android 開發中,我們碰到最多的就是列表了,一個APP中有簡單的列表,也有包含很多種Item的複雜列表。大多數的App首頁都是比較複雜的,比如一個社交APP的首頁,包含Banner區、廣告區、文字內容、圖片內容、視訊內容等等。RecyclerView 可以用ViewType 來區分不同的item,也可以滿足需求 ,但還是存在一些問題,比如:1,在item過多邏輯複雜列表介面,Adapter裡面的程式碼量龐大,邏輯複雜,後期難以維護。2,每次增加一個列表都需要增加一個Adapter,重複搬磚,效率低下。那麼本篇文章講的就是從這兩個維度去簡化我們的Adapter。

思路分析與實現過程

上面提出了兩個問題,接下來看一下我們怎麼去解決這兩個問題。首先我們來看一下我們常規的寫一個包含多item的複雜的列表介面(比如就是前面說的包含banner,廣告,文字內容,圖片內容,視訊內容的首頁):

/**
 * Created by zhouwei on 17/2/17.
 */

public class HomePageAdapter extends RecyclerView.Adapter {
    public static final int TYPE_BANNER = 0;
    public static final int TYPE_AD = 1;
public static final int TYPE_TEXT = 2;
    public static final int TYPE_IMAGE = 3;
    public static final int TYPE_VIDEO = 4;
    private List<HomePageEntry> mData;

    public void setData(List<HomePageEntry> data) {
        mData = data;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch (viewType){
            case TYPE_BANNER:
                return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_banner_layout,null));
            case TYPE_AD:
                return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_ad_item_layout,null));
            case TYPE_TEXT:
                return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_text_item_layout,null));
            case TYPE_IMAGE:
                return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_image_item_layout,null));
            case TYPE_VIDEO:
                return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_video_item_layout,null));
        }
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int type = getItemViewType(position);
        switch (type){
            case TYPE_BANNER:
                // banner 邏輯處理
                break;
            case TYPE_AD:
                // 廣告邏輯處理
                break;
            case TYPE_TEXT:
                // 文字邏輯處理
                break;
            case TYPE_IMAGE:
               //圖片邏輯處理
                break;
            case TYPE_VIDEO:
                //視訊邏輯處理
                break;
            // ... 此處省去N行程式碼
        }
    }

    @Override
    public int getItemViewType(int position) {
        if(position == 0){
            return TYPE_BANNER;//banner在開頭
        }else {
            return mData.get(position).type;//type 的值為TYPE_AD,TYPE_IMAGE,TYPE_AD,TYPE_VIDEO其中一個
        }

    }

    @Override
    public int getItemCount() {
        return mData == null ? 0:mData.size();
    }



    public static class BannerViewHolder extends RecyclerView.ViewHolder{

        public BannerViewHolder(View itemView) {
            super(itemView);
            //繫結控制元件
        }
    }

    public static class VideoViewHolder extends RecyclerView.ViewHolder{

        public VideoViewHolder(View itemView) {
            super(itemView);
            //繫結控制元件
        }
    }
    public static class AdViewHolder extends RecyclerView.ViewHolder{

        public AdViewHolder(View itemView) {
            super(itemView);
            //繫結控制元件
        }
    }
    public static class TextViewHolder extends RecyclerView.ViewHolder{

        public TextViewHolder(View itemView) {
            super(itemView);
            //繫結控制元件
        }
    }
    public static class ImageViewHolder extends RecyclerView.ViewHolder{

        public ImageViewHolder(View itemView) {
            super(itemView);
            //繫結控制元件
        }
    }
}複製程式碼

上面這樣就是我們通常寫一個多Item列表的方法,根據不同的ViewType 處理不同的item,如果邏輯複雜,這個類的程式碼量是很龐大的。如果版本迭代新增新的需求,修改程式碼很麻煩,後期維護困難。

其實可以看到,程式碼量在於每種item的檢視繫結、資料繫結和邏輯處理,並且這些操作都是Item共有的操作,既然是共有的,那麼我們就可以抽取出來。因此我們可以將Item抽象成為一個獨立的元件(我們將這個元件叫做Cell),它負責每個item的檢視繫結、資料繫結和邏輯處理,現在我們就可以把Adapter 中的程式碼放到Cell 中去了。Adapter 繫結檢視和繫結資料的操作呼叫對應位置的Cell的方法就可以了。

那我們來看一下Cell,Cell作為一個獨立元件,它有以下功能:
1,檢視繫結
2,資料繫結
3,資源釋放
4,獨立id(讓Adapter 區分是那種Cell)

1,首先我們定義一個頂層介面Cell,對應上面的4個功能,有4個方法

/**
 * Created by zhouwei on 17/1/19.
 */

public interface Cell {
    /**
     * 回收資源
     *
     */
    public void releaseResource();

    /**
     * 獲取viewType
     * @return
     */
    public  int getItemType();

    /**
     * 建立ViewHolder
     * @param parent
     * @param viewType
     * @return
     */
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

    /**
     * 資料繫結
     * @param holder
     * @param position
     */
    public  void onBindViewHolder(RecyclerView.ViewHolder holder, int position);
}複製程式碼

2,既然可以繫結資料,我們要給它資料來源,因此定義一個基類RVBaseCell,儲存一個範型資料實體

public  abstract class RVBaseCell<T> implements Cell {

    public RVBaseCell(T t){
        mData = t;
    }
    public T mData;

    @Override
    public void releaseResource() {
        // do nothing
        // 如果有需要回收的資源,子類自己實現
    }
}複製程式碼

3,將原來Adapter 中的檢視繫結、資料繫結、邏輯處理等操作,放到對應的Cell 中去做
例如,我們為Banner建立一個Cell ,叫BannerCell。程式碼如下:

/**
 * Created by zhouwei on 17/2/17.
 */

public class BannerCell extends RVBaseCell<HomePageEntry>{
    public static final int TYPE_BANNER = 0;
    public BannerCell(HomePageEntry homePageEntry) {
        super(homePageEntry);
    }

    @Override
    public int getItemType() {
        return TYPE_BANNER;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_banner_layout,null));
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        //處理banner 邏輯
    }

    public static class BannerViewHolder extends RecyclerView.ViewHolder{

        public BannerViewHolder(View itemView) {
            super(itemView);
            //繫結控制元件
        }
    }
}複製程式碼

4,改造Adapter,Adapter中儲存的不再是實體資料,而是一個Cell列表,改造Adapter 回撥方法,呼叫對應位置的Cell中的方法。
程式碼如下:


public  class HomePageAdapter<C extends RVBaseCell> extends RecyclerView.Adapter {
    private List<C> mData;

    public void setData(List<C> data) {
        mData = data;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        for(int i=0;i<getItemCount();i++){
            if(viewType == mData.get(i).getItemType()){
                return mData.get(i).onCreateViewHolder(parent,viewType);
            }
        }

        throw new RuntimeException("wrong viewType");
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

    }

    @Override
    public int getItemViewType(int position) {
        return mData.get(position).getItemType();

    }

    @Override
    public int getItemCount() {
        return mData == null ? 0:mData.size();
    }

}複製程式碼

現在看一下這個HomePageAdapter 的程式碼和之前寫的 HomePageAdapter的程式碼,可以把一個上千行程式碼的Adapter 縮減為只有幾十行程式碼,並且更重要的是,這個Adapter 是一個通用的,我們所有的列表只需要這麼一個Adapter 就OK了,只需要新增Cell就行。這樣就把我們文章開始提到的兩個問題都解決了。

我們可以用兩個圖來對比一下,以前新增一個列表就要寫一個Adapter:

RecyclerView 之 Adapter 的簡化過程淺析
old_adapter.png

現在是這樣的:

RecyclerView 之 Adapter 的簡化過程淺析
new_adapter.png

如上圖所示,只需要一個Adapter就行, 就像是Adapter上面有很多插槽,我們將一個個Cell插到Adapter上,即插即用。完全解耦,以後新增需求和砍掉需求,只需要增加一種Cell 或者減少一種Cell就行,不用動以前的老程式碼,維護方便。

最後

以上就是RecyclerView Adapter 的簡化封裝過程與思路分析,文章中的程式碼只放了部分,詳細的封裝程式碼請看GithubCustomAdapter。CustomAdapter也整合了更多的功能,歡迎start。

相關文章