優雅的實現多類型列表的Adapter

引言

在開發中經常會遇到,一個列表(RecyclerView)中有多種布局類型的情況。前段時間,看到了這篇文章

[譯]關于 Android Adapter,你的實現方式可能一直都有問題

文中主要從設計的角度闡釋如何更合理的實現多種布局類型的Adapter,本文主要從實踐的角度出發,站在巨人的肩膀上,結合我個人的理解進行闡述,如果有紕漏,歡迎留言指出。

有多種布局類型

有時候,由于應用場景的需要,列表(RecyclerView)中需要存在一種以上的布局類型。為了闡述的方便,我們先假設一種應用場景

列表中含有若干個常規的布局,在列表的中的第一個位置與第二個位置中分別為兩個不同的布局,其余為常規的布局

針對這樣的需求,筆者一直以來的實現方式如下

private final int ITEM_TYPE_ONE = 1;
private final int ITEM_TYPE_TWO = 2;

@Override
public int getItemViewType(int position) {
    if(0 == position){
       return  ITEM_TYPE_ONE;
    }else if(1 == position){
        return ITEM_TYPE_TWO;
    }
    return super.getItemViewType(position);
}
@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(ITEM_TYPE_ONE == viewType){
            return new OneViewHolder();
        }else if(ITEM_TYPE_TWO == viewType){
            return new TwoViewHolder();
        }
        return new NormalViewHolder();            
    }
@Override
//偽代碼
    public void onBindViewHolder(ViewHolder holder, int position) {
       if(holder instanceof OneViewHolder ){
             ...
        }else if(holder instanceof TwoViewHolder){
             ...
        }else{
            ...
        }
    }
  • 在Adapter的getItemViewType方法中返回特定位置的特定標識(根據前文需求,就是position0與position1)
  • 在onCreateViewHolder中根據viewType參數,也就是getItemViewType的返回值來判斷需要創建的ViewHolder類型
  • 在onBindViewHolder方法中對ViewHolder的具體類型進行判斷,分別為不同類型的ViewHolder進行綁定數據與邏輯處理

通過以上就能實現多類型列表的Adapter,但這樣的代碼寫多了總會覺得別扭,特別是看到了[譯]關于 Android Adapter,你的實現方式可能一直都有問題這篇文章之后。

結合文章與我個人的理解,這種實現方式所存在弊端可以總結為以下幾點:

  • 類型檢查與類型轉型,由于在onCreateViewHolder根據不同類型創建了不同的ViewHolder,所以在onBindViewHolder需要針對不同類型的ViewHolder進行數據綁定與邏輯處理,這導致需要通過instanceof對ViewHolder進行類型檢查與類型轉型。
    [譯]關于 Android Adapter,你的實現方式可能一直都有問題中是這樣說的

許多年前,我在我的顯示器上貼了許多的名言。其中的一個來自 Scott Meyers 寫的《Effective C++》 這本書(最好的IT書籍之一),它是這么說的:
不管什么時候,只要你發現自己寫的代碼類似于 “ if the object is of type T1, then do something, but if it’s of type T2, then do something else ”,就給自己一耳光

  • 不利于擴展,目前的需求是列表中存在三種布局類類型,那么如果需求變動,極端一點的情況就是數據源是從服務器獲取的,數據中的model決定列表中的布局類型。這種情況下,每當model改變或model類型增加,我們都要去改變adapter中很多的代碼,同時Adapter還必須知道特定的model在列表中的位置(position)除非跟服務端約定好,model(位置)不變,很顯然,這是不現實的。
    [譯]關于 Android Adapter,你的實現方式可能一直都有問題中是這樣說的

另外,我們實行那些 adapter 的方法違背了 SOLID 原則中的“開閉準則” 。它是這樣說的:“對擴展開放,對修改封閉。” 當我們添加另一個類型或者 model 到我們的類中時,比如叫 Rabbit 和 RabbitViewHolder,我們不得不在 Adapter 里改變許多的方法。 這是對開閉原則明顯的違背。添加新對象不應該修改已存在的方法。

  • 不利于維護,這點應該是上一點的延伸,隨著列表中布局類型的增加與變更,getItemViewType、onCreateViewHolder、onBindViewHolder中的代碼都需要變更或增加,Adapter 中的代碼會變得臃腫與混亂,增加了代碼的維護成本。

首先讓我摸摸自己的臉,然后結合[譯]關于 Android Adapter,你的實現方式可能一直都有問題,看看如何優雅的實現多類型列表的Adapter

優雅的實現

結合上文,我們的核心目的就是三個

  • 避免類的類型檢查與類型轉型
  • 增強Adapter的擴展性
  • 增強Adapter的可維護性

前文提到了,當列表中類型增加或減少時Adapter中主要改動的就是getItemViewType、onCreateViewHolder、onBindViewHolder這三個方法,因此,我們就從這三個方法中開始著手。

Talk is cheap. Show me the code,圍繞以上幾點,開始碼代碼

getItemViewType

原本的代碼是這樣

@Override
public int getItemViewType(int position) {
    if(0 == position){
       return  ITEM_TYPE_ONE;
    }else if(1 == position){
        return ITEM_TYPE_TWO;
    }
    return super.getItemViewType(position);
}

在這段代碼中,我們必須知道特定的布局類型在列表中的位置,而布局類型在列表中的位置是由數據源決定的,為了解決這個問題并且減少if之類的邏輯判斷簡化代碼,我們可以簡單粗暴的在Model中增加type標識,優化之后getItemViewType的實現大致如下

@Override
 public int getItemViewType(int position) {
    return modelList.get(position).getType();
 }

這樣的方式有很大的局限性(誰用誰知道),這里就不展開了,直接看正確的姿勢,先看代碼(具體可以看源碼

public interface Visitable {
    int type(TypeFactory typeFactory);
}

public class One implements Visitable {
    ...
    ...
    @Override
    public int type(TypeFactory typeFactory) {
        return typeFactory.type(this);
    }
}

public class Two implements Visitable {
    ...
    ...
    @Override
    public int type(TypeFactory typeFactory) {
        return typeFactory.type(this);
    }
}

public class Normal implements Visitable{
    ...
    ...
    @Override
    public int type(TypeFactory typeFactory) {
        return typeFactory.type(this);
    }
}

public interface TypeFactory {
    int type(One one);

    int type(Two two);
}

public class TypeFactoryForList implements TypeFactory {
    private final int TYPE_RESOURCE_ONE = R.layout.layout_item_one;
    private final int TYPE_RESOURCE_TWO = R.layout.layout_item_two;
    private final int TYPE_RESOURCE_NORMAL = R.layout.layout_item_normal;
    @Override
    public int type(One one) {
        return TYPE_RESOURCE_ONE;
    }

    @Override
    public int type(Two one) {
        return TYPE_RESOURCE_TWO;
    }

    @Override
    public int type(Normal normal) {
        return TYPE_RESOURCE_NORMAL;
    }
    ...
}

針對getItemViewType可以進行如下實現

private List<Visitable> modelList;
@Override
public int getItemViewType(int position) {
    return modelList.get(position).type(typeFactory);
 }

小結

  • 通過接口抽象,將所有與列表相關的Model抽象為Visitable,當我們在初始化數據源時就能以List<Visitable>的形式將不同類型的Model集合在列表中;
  • 通過訪問者模式,將列表類型判斷的相關代碼抽取到TypeFactoryForList 中,同時所有列表類型對應的布局資源都在這個類中進行管理與維護,以這樣的方式巧妙的增強了擴展性與可維護性;
  • getItemViewType中不再需要進行if判斷,通過數據源控制列表的布局類型,同時返回的不再是簡單的布局類型標識,而是布局的資源ID(通過modelList.get(position).type()獲取),進一步簡化代碼(在onCreateViewHolder中會體現出來);

onCreateViewHolder

結合上文可以了解到,getItemViewType返回的是布局資源ID,也就是onCreateViewHolder(ViewGroup parent, int viewType)參數中的viewType,我們可以直接用viewType創建itemView,但是,問題來了,itemView創建之后,還是需要進行類型判斷,創建不同的ViewHolder,針對這個問題可以分以下幾個步驟解決
首先為了增強ViewHolder的靈活性,可以繼承RecyclerView.ViewHolder派生出BaseViewHolder抽象類如下

public abstract class BaseViewHolder<T> extends RecyclerView.ViewHolder {
    private SparseArray<View> views;
    private View mItemView;
    public BaseViewHolder(View itemView) {
        super(itemView);
        views = new SparseArray<>();
        this.mItemView = itemView;
    }

    public View getView(int resID) {
        View view = views.get(resID);

        if (view == null) {
            view = mItemView.findViewById(resID);
            views.put(resID,view);
        }

        return view;
    }

    public abstract void setUpView(T model, int position, MultiTypeAdapter adapter);
}

不同的ViewHolder繼承BaseViewHolder并實現setUpView方法即可。

然后對TypeFactory 與TypeFactoryForList 增加如下代碼

public interface TypeFactory {
  ...
  BaseViewHolder createViewHolder(int type, View itemView);
}

public class TypeFactoryForList implements TypeFactory {
  private final int TYPE_RESOURCE_ONE = R.layout.layout_item_one;
  private final int TYPE_RESOURCE_TWO = R.layout.layout_item_two;
  private final int TYPE_RESOURCE_NORMAL = R.layout.layout_item_normal;
  ...
  @Override
  public BaseViewHolder createViewHolder(int type, View itemView) {

        if(TYPE_RESOURCE_ONE == type){
            return new OneViewHolder(itemView);
        }else if (TYPE_RESOURCE_TWO == type){
            return new TwoViewHolder(itemView);
        }else if (TYPE_RESOURCE_NORMAL == type){
            return new NormalViewHolder(itemView);
        }

        return null;
    }
}

最后對onCreateViewHolder方法進行如下實現

@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    Context context = parent.getContext();

    View itemView = View.inflate(context,viewType,null);
    return typeFactory.createViewHolder(viewType,itemView);
}

小結

  • 在onCreateViewHolder中以BaseViewHolder作為返回值類型。因為BaseViewHolder作為不同類型的ViewHolder的基類,可以避免在onBindViewHolder中對ViewHolder進行類型檢查與類型轉換,同時也可以簡化onBindViewHolder方法中的代碼(具體會在下文闡述);
  • 創建不同類型的ViewHolder的相關代碼被抽取到了TypeFactoryForList 中,簡化了onCreateViewHolder中的代碼,同時與類型相關的代碼都集中在TypeFactoryForList 中,方便后期維護與拓展;

onBindViewHolder

經過以上實現,onBindViewHolder中的代碼就非常的輕盈了,如下

@Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
    holder.setUpView(models.get(position),position,this);
}

可以看到,在onBindViewHolder中不需要對ViewHolder進行類型檢查與轉換,也不需要針對不同類型的ViewHoler執行不同綁定操作,不同的列表布局類型的數據綁定(邏輯代碼)都交給了與其自身對應的ViewHolder處理,如下(setUpView中的代碼可根據實際情況修改)

public class NormalViewHolder extends BaseViewHolder<Normal> {
    public NormalViewHolder(View itemView) {
        super(itemView);
    }

    @Override
    public void setUpView(final Normal model, int position, MultiTypeAdapter adapter) {
        final TextView textView = (TextView) getView(R.id.normal_title);
        textView.setText(model.getText());

        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(textView.getContext(),model.getText(),Toast.LENGTH_SHORT).show();
            }
        });
    }
}

小結

  • onBindViewHolder中不需要進行類型檢查與轉換,對ItemView的數據綁定與邏輯處理都交由各自的ViewHolder進行處理。通過這樣方式,讓代碼更整潔,更易于維護,同時也增強了擴展性。

總結

經過如上優化之后,Adapter中的代碼如下

public class MultiTypeAdapter extends RecyclerView.Adapter<BaseViewHolder> {
    private TypeFactory typeFactory;
    private List<Visitable> models;

    public MultiTypeAdapter(List<Visitable> models) {
        this.models = models;
        this.typeFactory = new TypeFactoryForList();

    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Context context = parent.getContext();

        View itemView = View.inflate(context,viewType,null);
        return typeFactory.createViewHolder(viewType,itemView);
    }

    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        holder.setUpView(models.get(position),position,this);
    }

    @Override
    public int getItemCount() {
        if(null == models){
            return 0; 
        }
        return models.size();
    }


    @Override
    public int getItemViewType(int position) {
        return models.get(position).type(typeFactory);
    }
    
}

當列表中增加類型時:

  • 為該類型創建實現了Visitable接口的Model類
  • 創建繼承于BaseViewHolder的ViewHolder(與Model類對應)
  • 為TypeFactory增加type方法(與Model類對應) ,同時TypeFactoryForList 實現該方法
  • 為TypeFactoryForList增加與列表類型對應的資源ID參數
  • 修改TypeFactoryForList 中的createViewHolder方法

可以看到,雖然Adapter中的代碼量減少,但總體的代碼量并沒減少(可能還增多了),但是和好處比起來,增加一點代碼量還是值得的

  • 拓展性——Adapter并不關心不同的列表類型在列表中的位置,因此對于Adapter來說列表類型可以隨意增加或減少,我們只需要維護好數據源即可。
  • 可維護性——不同的列表類型由不同的ViewHolder維護,相互之間互不干擾;對類型的管理都在TypeFactoryForList 中,TypeFactoryForList 中的代碼量少,代碼簡潔,維護成本低。
  • 避免了類的類型檢查與類型轉型,這點看源碼就可以知道

源碼地址

最后

可能還有待完善的地方,大家可以根據實際情況進行修改與擴展。同時,歡迎留言交流。

參考:
[譯]關于 Android Adapter,你的實現方式可能一直都有問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容