引言
在開發中經常會遇到,一個列表(RecyclerView)中有多種布局類型的情況。前段時間,看到了這篇文章
文中主要從設計的角度闡釋如何更合理的實現多種布局類型的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 中的代碼量少,代碼簡潔,維護成本低。
- 避免了類的類型檢查與類型轉型,這點看源碼就可以知道
最后
可能還有待完善的地方,大家可以根據實際情況進行修改與擴展。同時,歡迎留言交流。