RecyclerView控件從2014發布以來,目前已經普遍用于項目中,來承載各種列表內容。同時,列表樣式也隨著項目變的越來越復雜,從簡單統一的列表,變化成頭部、腳部、不同類型的Item互相組合。本文將通過一些開源庫來學習一下如何實現各種復雜類型的列表,分析了viewType應該如何與視圖、數據相綁定,并將業務邏輯單獨分離。
初步實現
問題的開始是這樣的:項目里有個頁面,整個列表采用ListView
實現,除了常規的列表項外,還有兩個自定義的View
也要隨著頁面滑動。Ok,listView
支持addHead
,而且還是多head,自定義view
通過addHead
方法添加到listview
中,就一切ok。然而ListView
畢竟漸漸過時了,打算采用RecyclerView
來重構一下。雖然RecyclerView
不支持addHead
這種方法,但是可以通過getItemViewType
方法來實現返回多種類型。
@Override
public int getItemViewType(int position) {
switch (position) {
case 0:
return TYPE_HEAD1;
case 1:
return TYPE_HEAD2;
case 2:
return TYPE_ITEM;
default:
return TYPE_ITEM;
}
}
即根據業務需求,返回不同的類型的值,那么下一步,我們同時需要在onCreateViewHolder
中針對不同的viewType
來創建不同的ViewHolder
,同樣的,在onBindViewHolder
中,也要處理不同的類型,特別的,如果不同類型的viewholder
具有不同的方法的情況,還需要針對viewholder
做一次類型轉換。類似這樣:
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_HEAD1) {
((Head1VH) holder).bindData();
} else if (getItemViewType(position) == TYPE_HEAD2) {
((Head2VH) holder).bindData();
} else if (getItemViewType(position) == TYPE_ITEM) {
((Item) holder).bindData();
}
}
以上就是一般RecyclerView
中實現多類型Item的方法,相應的變化一下,把頭部和腳部當作特定類型的ItemType,并提供public
方法共外部setHead
即可支持添加頭部。
問題進階
上述的方法,是解決了特定業務情景下的問題,但是很明細不利于擴展和維護。首先,當列表除了頭部外的部分依然會出現不同類型時,并且實際情況中,不同類型應該都是由服務器回傳的數據來決定的,我們就不能在getItemViewType中簡單的定義類型值來判斷。
一個可能的做法是,在數據層里添加type字段,通過type字段來
@Override
public int getItemViewType(int position) {
return datas.get(position).type;
}
然而在數據層包裹展示層需要的type字段并不是一個優雅的做法,它破壞了單一職責。同時,這么做也無法解決另一個問題:擴展性。
所謂擴展性就是Adapter最好能在數據類型變化時候,內部實現邏輯不需要改變,只是外部添加新的功能即可。那么這就要求Adapter對數據層是解耦的,不能顯式的持有外部的數據。Adapter設計之初,是為了兼容千變萬化的數據結構,并不是千變萬化的類型結構,因此,應該考慮把不同類型的變化從Adapter內部隔離開。
GitHub上關于多類型Item的RecyclerView的實現有很多庫,基本的思路是通過一個Manager類來管理多種類型中:數據和視圖的對應關系。實際上,都是圍繞如何解決viewType、數據、視圖的對應關系來進行一系列的封裝。
下面介紹兩個實現的比較簡潔而靈活的庫:
AdapterDelegates的思路是使用自定義的Adapter來“hook”原來的RecyclerView的Adapter,主要的Adapter方法如onBindViewHolder和onCreateViewHolder方法都被劫持使用adpter內部的一個Manager類來實現,參看下面的類圖會更加容易理解。
上圖是這個庫的基本類圖,省略了兩個非必要的類,其中只列出了一些典型的方法和對象。以onBindViewHolder()為例,可以看到從最頂層開始,這個方法會一步步往下調用,一直到AdapterDelegate這層,這一層也是最終面向使用者需要關心的層次,通過繼承抽象類AdapterDelegate,實現其中的方法,來完成業務邏輯和UI表現,代碼如下,和普通的RV.Adapter方法沒有區別:
public class NormalDelegate extends AbsListItemAdapterDelegate<NormalItem, Item, NormalDelegate.NormalItemVH> {
@NonNull
@Override
protected NormalItemVH onCreateViewHolder(@NonNull ViewGroup parent) {
return new NormalItemVH(inflater.inflate(R.layout.normal_item, parent, false));
}
@Override
protected void onBindViewHolder(@NonNull NormalItem item, @NonNull final NormalItemVH viewHolder, @NonNull List<Object> payloads) {
viewHolder.imageView.setImageResource(item.resId);
viewHolder.imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DetailsActivity.startActivity(view.getContext());
}
});
viewHolder.textView.setText(item.content);
viewHolder.textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String old = viewHolder.textView.getText().toString();
viewHolder.textView.setText(old + " " + (int) (10 * Math.random()));
}
});
}
}
但是通過這一層的封裝,成功的把多類型的情況分隔開,每種類型只需要在各種的AdapterDelegate中去編寫業務邏輯就可以,Adapter中的職業就非常簡單,只需要持有AdapterDelegateManager,由這個Manager類來維護每種類型具體對應的AdapterDelegate,而由AdapterDelegate維護UI和數據的綁定關系。
如此,面對多類型的情況或者在已有的業務基礎上增加了新的類型,都不再用去修改Adapter的基本實現,只要做兩件事:
- 編寫類型的AdapterDelegate來實現UI展示、數據綁定、點擊事件等工作
- 通過AdapterDelegateManager注冊新的AdapterDelegate
下面是一個demo例子(gif畫質比較渣,將就著看。。)
整個列表是一個RecyclerView,包含了兩種不同類型的頭部,簡單的Item類型和可橫向滑動展示的Item類型共計4種。來看看這個RecyclerView的Adapter實現:
class ItemList2Adapter extends ListDelegationAdapter<List<Item>> {
Activity activity;
List<Item> datas;
public ItemList2Adapter(Activity activity, List<Item> datas) {
this.activity = activity;
this.datas = datas;
delegatesManager.addDelegate(new Head1Delegate(activity))
.addDelegate(new Head2Delegate(activity))
.addDelegate(new NormalDelegate(activity))
.addDelegate(new HorizontalItemDelegate(activity));
setItems(datas);
}
}
從代碼里可以看到,整個Adapter是非常簡潔和清晰的,業務邏輯歸于Delegate當中解決,viewType和類型的映射關系放到delegateManager中處理。具體Delegate的代碼就不貼了,和常規單類型Adapter的寫法一致。下面再看看另一個庫的思路:MuliTypeAdapter.
這里就不自己畫類圖了,從其作者的文檔中引用一幅圖,如下:
從上文所說的基本原則來分析,我們應重點關注其如何實現viewType字段和類型的映射,以及如何和RV.Adaper交互。從類名和繼承關系來看,我們可以知道,MultiTypeAdapter應該是充當之前所說的Manage的角色,同時,這個類實現了兩個接口:
TypePool
FlatTypeAdapter
因此,維護viewType和類型映射關系就必然會體現在其中。而類Items是一個繼承ArrayList<Object>的空類,表明了這個類將是所有數據結構的基類。最后,唯一單獨沒有聯系的ItemViewProvider<C,V>則可以推斷為用來實現業務邏輯和UI展示。如此,基本要素都一一對應上,接下來看看它是如何實現其中的功能。
public class MultiTypeAdapter extends RecyclerView.Adapter<ViewHolder>{
@Override
public int getItemViewType(int position) {
Object item = items.get(position);
return indexOf(flattenClass(item));
}
@Override public ViewHolder onCreateViewHolder(ViewGroup parent, int indexViewType) {
if (inflater == null) {
inflater = LayoutInflater.from(parent.getContext());
}
ItemViewProvider provider = getProviderByIndex(indexViewType);
provider.adapter = MultiTypeAdapter.this;
return provider.onCreateViewHolder(inflater, parent);
}
@SuppressWarnings("unchecked") @Override
public void onBindViewHolder(ViewHolder holder, int position) {
Object item = items.get(position);
ItemViewProvider provider = getProviderByClass(flattenClass(item));
provider.onBindViewHolder(holder, flattenItem(item));
}
}
從MuliTypeAdapter的幾個重點方法可以看出,其調用的方法幾乎都是接口或者抽象類的空方法,這側面體現出來此庫的高度可定制性,所有的方法實現都可以由具體的實現類來決定。
從getViewType方法中可以看到,其返回值由indexOf方法確定,而這個方法定義在TypePool接口中,由MultiTypePool實現,當然我們也可以自己實現然后替換掉。從MultiTypePool的源碼中分析:
private ArrayList<Class<?>> contents;
private ArrayList<ItemViewProvider> providers;
public void register(Class<?> clazz,ItemViewProvider provider) {
if (!contents.contains(clazz)) {
contents.add(clazz);
providers.add(provider);
} else {
int index = contents.indexOf(clazz);
providers.set(index, provider);
Log.w(TAG, "You have registered the " + clazz.getSimpleName() + " type. " +
"It will override the original provider.");
}
}
@Override
public int indexOf(Class<?> clazz) {
int index = contents.indexOf(clazz);
if (index >= 0) {
return index;
}
for (int i = 0; i < contents.size(); i++) {
if (contents.get(i).isAssignableFrom(clazz)) {
return i;
}
}
return index;
}
可以看到,不同于AdapteDelegate中綁定viewType和Delegate,在這里,它將數據類Class和ItemViewProvider進行了綁定,分別用兩個ArrayList來存儲對象,用index索引作為viewType的值。如下圖示意:
當Adapter中注冊類型時,將兩者綁定;getViewType時,則首先通過position拿到數據類型,再通過數據類型拿到對應的UI類型;onBindViewHolder時,同樣通過position拿到數據類型,拿到ItemViewProvider,繼而調用ItemViewProvider的onBindViewHolder方法去交由實現類處理。以上應該可以基本明白該庫是如何維護viewType、數據類型和UI類型的映射關系的。
而在編寫Adapter的過程中,特別是多類型的Adapter過程中,常常會發現自己不得不在onBindVieHolder方法中,對holder轉型來調用其內部方法,或者對數據轉型來使用其字段值,大量的類型轉換既顯得臃腫又影響速度。既然我們已經把不同類型的情況已經獨立成一個個ItemViewProvider(或者AdapterDelegate,另一個庫中的稱呼),那么在相應的實現類中,我們也希望能正確的分發數據類型和視圖類型。
在AdatperDelegates庫中,如果我們的業務實現類直接繼承與AdapterDelegate來編寫,是這樣的:
public class Head1Delegate extends AdapterDelegate<List<Item>> {
@Override
protected void onBindViewHolder(@NonNull List<Item> items,
int position, @NonNull RecyclerView.ViewHolder holder,
@NonNull List<Object> payloads) {
((Head1VH) holder).imageView.
setImageResource(((Head1) items.get(position)).getResId());
}
}
可以看到還是沒有避免類型轉換。作者其實也意識到這點,因此提供了一個AbsListItemAdapterDelegate類來供我們繼承,其內部通過泛型預先幫我們做好類型轉換,再分發下去:
public abstract class AbsListItemAdapterDelegate<I extends T, T,
VH extends RecyclerView.ViewHolder>
extends AdapterDelegate<List<T>> {
@Override
protected final void onBindViewHolder(@NonNull List<T> items, int position,
@NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
onBindViewHolder((I) items.get(position), (VH) holder, payloads);
}
MuliTypeAdapter則干脆的多,在定義ItemViewProvider的抽象方法時就已經考慮了這個問題,解決方案和上述一致,但是寫法上看起來更為優雅:
protected abstract void onBindViewHolder(@NonNull V holder, @NonNull T t);
當然,這樣做本質是在內層做好轉型再分發,如果要真正意思上的避免轉型,可以采用訪問者模式(參見:Writing Better Adapter)
關于MuliTypeAdapter的Demo就不做了,其官方上例子已經很詳盡。并且,除了之前提到的核心邏輯外,其還提供了全局類型池設計、數據二次分發設計(即沒有討論的FlatTypeAdapter
接口),感興趣的可以繼續了解。
上述兩個庫,都做到了對不同類型Item的分離,每次組裝一個列表時,只需要把數據源正確的組裝好,adapter內部會通過各自實現的Manager來定位對應的UI來展示。在實際開發中,可能的問題或許是不同Item之間的關聯性,比如一個頭部類型的帶有聯動其他Item的交互的話,就需要打破這種獨立性(此時需要通過構造函數等方法傳入其他對象的實例)。另外,對于常見的頭部、列表、腳部的需求來說,實際上在此都是當作三種類型來處理,那么對于服務器回傳的列表數據,我們需要自行包裹上頭部、腳部的數據類型,這樣才能正確的被處理,也是相對麻煩之處。
參考文章: