優雅的實現多類型列表的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,你的實現方式可能一直都有問題

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

推薦閱讀更多精彩內容