借助 android databinding 框架,逃離 adapter 和 viewholder 的噩夢
前言
理解這篇文章最好有 databinding 的基礎知識,如果之前沒有了解過,推薦下面三篇文章:
但你也可以先看完這篇文章,大致了解 databinding 可以怎么幫助到我們后,再回去看這些基本介紹。
對于 android 開發者來說,諸多噩夢中,adapter 和 viewholder 絕對算一個,倒不是難,而是煩,千遍一率的相似代碼,對于程序員來說,重復是最令人討厭的。所以這才有這么多仁人志士,前俯后繼地進行通用型,萬能型 adapter 的抽象 。
但是,由于受限于傳統的 android 開發,即使他們已經做得很極致了,但用起來還是不夠爽,我們還是要不停地去實現 ViewHolder。
直到我開始學習 databinding 框架 (我現在有一種趕腳,它簡直就是人民,哦,不對,android 開發者的大救星啊),在 棉花糖給 Android 帶來的 Data Bindings 這篇文章中說到,databinding 可以輕松地實現多類型的 viewholder 時,我在想,我干嘛不用它來解決這個世界性的大難題呢(呵呵)。當下就立馬開始嘗試,實際,只要你寫起來,就會發現,實現起來是非常 easy 和自然的 (但我不知道為什么沒有人這么去做,我 google/github 了也沒看到類似的,讓我陷入了深深地懷疑,難道是我做錯了,還懇請各位大神來 review 我的代碼并打臉我)。
使用 databinding 框架實現的 adapter 只有 100 行左右的代碼,單一 java 文件,理解起來非常容易。從此你就可以忘記 viewholder 了。
當然,根據代碼守恒定律,代碼總量總體是恒定的,我們能使用這么少的代碼,背后是 databinding 框架為我們生成的大量代碼,眼不見,心不煩,更何況這是 google 為我們生成的代碼。
是不是已經按捺不住激動的心情啦。好吧,我們言歸正轉。其實在之前我已經寫 一篇文章 來闡述這種思想,并寫了一個簡陋的例子,但后來我還是覺得這個例子太簡單了,說服力不夠,沒有解決很多人關心的問題,在每個通用型 adapter 的文章下面,都有一堆人在問,如果用你這個 adapter,怎么刷新,怎么加載更多,怎么實現 header item,怎么實現 footer item。那么,我就決定,寫一個完整的例子,一次性解決你們所有的疑問。并且,我把這個過程寫下來,如何一步步實現這些功能,如何一步步重構代碼,使代碼優雅。最后,即使沒有打動你投入 databinding 的懷抱,我覺得你也可以學習到很多。
先來看一下最后的效果,UI 就別吐槽了,畢竟對于工程師來說,獨立開發一個應用,最大的問題就是選顏色了(其實是我啦),本來想用一個 GIF 來展示所有效果的,但一個 GIF 體積太大了,所以就拆成了三個:
-
演示 HeaderItem,EmptyItem,ErrorItem,刷新,加載更多,加載出錯并重試:
-
演示 沒有更多數據:
-
稍微潤色了一下 UI,為數據 Item 增加各種點擊事件:
這個完整的例子包括了以下功能,我想應該能滿足 90% 的需求吧:
-
刷新
-
加載更多
-
支持 header item
-
支持 empty item,并允許再次刷新
-
支持 error item,并允許再次刷新
-
支持 footer item,包括 3 種狀態:加載中,出錯并允許重試,沒有更多數據
-
支持多種數據類型的 item,我們在這個例子中只展示了 ImageItem 和 TextItem 兩種類型
這個項目中有兩個文件夾,MultiTyepAdapterSample 和 MultiTypeAdapterTutorial,兩者代碼是幾乎相同的,后者是我為了寫這篇文章重新創建的,每一個關鍵步驟我都打好了 tag,以方便讀者進行對照。
PS: 下面的內容完全是基于 databinding 思想來展開講的,所以如果你對它不感興趣,那么下面的內容對你就沒什么幫助。另外,文章比較長且啰索 (力爭講解到每一個細節),需要比較充裕的時間來閱讀和練習。如果你對 databinding 感興趣,歡迎提交 issue 探討。
大綱
-
-
一步一步實現極簡的 adapter
-
-
- </ol> </li>
-
- </ol> </li>
- </ol>
實現篇
一步一步實現極簡的 adapter
首先創建一個新的 Android Studio 工程,在 app module 的 build.gradle 中加上 databinding 的支持,并導入 recyclerview support 庫:
// build.gradle android { //...
dataBinding { enabled = true }
}
dependencies { //... compile 'com.android.support:recyclerview-v7:25.1.0' }</code></pre>
接著,我們來開始來實現這個 adapter,取名為 MultiTypeAdapter,因為我們要支持多類型,那么 adapter 里的 item 必然是抽象的,我們定義為 IItem:
// MultiTypeAdapter.java public interface IItem { }
private List<IItem> items = new ArrayList<>();</code></pre>
我們先從簡單的入手,先來看看 getItemCount() ,我們用 showHeader 和 showFooter 兩個變量來控制是否顯示 header 或 footer,那么 getItemCount() 的實現如下:
// MultiTypeAdapter.java @Override public int getItemCount() { int cnt = items.size(); if (showHeader) { cnt++; } if (showFooter) { cnt++; } return cnt; }
接著來實現 getItemViewType(int position) ,關于這個方法,一般的實現,我們要根據 position 和相應位置的 item 類型來返回不同的值,比如:
// MultiTypeAdapter.java @Override public int getItemViewType(int position) { if (position == 0 && showHeader) { return ITEM_TYPE_HEADER; } else if (position == getItemCount() -1 && showFooter) { return ITEM_TYPE_FOOTER; } else { if (items.get(position) instanceof ImageItem) { return ITEM_TYPE_IMAGE; } else { return ITEM_TYPE_TEXT; } } }
這樣的實現,很煩很丑是不是。關于這個方法的優化,我們很容易達成一種共識,首先,我們不再返回類似 ITEM_TYPE_IMAGE 這種常量類型,而是直接返回它的 xml layout,其次,我們直接從 item 自身得到這個 layout。因此,我們為 IItem 增加一個 getType() 的接口方法。代碼如下:
public interface IItem { // should directly return layout int getType(); }
@Override public int getItemViewType(int position) { if (position == 0 && showHeader) { return R.layout.item_header; } else if (position == getItemCount() -1 && showFooter) { return R.layout.item_footer; } else { return items.get(position).getType(); } }</code></pre>
(2017/2/15 Update: 由于 getType() 實際是應該返回一個 xml layout 的,為了讓這個方法名意義更明確,從 1.0.7 開始,這個方法重命名為 getLayout() ,但整個教程仍然保留為 getType() )
因為 header 和 footer,尤其是 footer,只是單純地用來顯示 正在 loading 等一些狀態,我們很容易把它跟常規的數據 item 區別對待,但是,實際上我們可以把它看成一個偽 item,沒有數據,只有布局的 item。我們分別實現只有布局的 HeaerItem 和 FooterItem,并在合適的時機加到 items 里面或從 items 里移除,就可以控制 header 和 footer 的顯示與隱藏了。
// HeaerItem.java public class HeaderItem implements MulitTypeAdapter.IItem { @Override public int getType() { return R.layout.item_header; } }
// FooterItem.java public class FooterItem implements MulitTypeAdapter.IItem { @Override public int getType() { return R.layout.item_footer; } }</code></pre>
這樣,我們的 getItemViewType() 終于可以簡化成一行代碼了,清爽!
@Override public int getItemViewType(int position) { return items.get(position).getType(); }
這樣,我們也不需要 showHeader 和 showFooter 這樣的狀態變量了,那么 getItemCount() 也可以簡化成一行代碼了。
public int getItemCount() { return items.size(); }
剛才說到我們要在合適的時機把 HeaerItem 或 FooterItem 加到 items 或從 items 中移除,所以我們給 adapter 加上一些操作 items 的方法。如下所示:
// MultiTypeAdapter.java public void setItem(IItem item) { clearItems(); addItem(item); }
public void setItems(List<IItem> items) { clearItems(); addItems(items); }
public void addItem(IItem item) { items.add(item); }
public void addItem(IItem item, int index) { items.add(index, item); }
public void addItems(List<IItem> items) { this.items.addAll(items); }
public void removeItem(IItem item) { items.remove(item); }
public void clearItems() { items.clear(); }</code></pre>
你可能會想,誒,在這些操作函數里最后再加上 notifyDatasetChanged() 是不是會更方便點,這樣我在上層就不用再手動調用一下 adapter.notifyDatasetChanged() ,實際當你自己寫起來的時候,你就會發現這樣并不靈活。因為,我可能并不想每一次 addItem 都刷新一次 UI,我可能要多次 addItem 后才刷新一次 UI,這樣,在上層由調用者來決定何時刷新 UI 會更靈活,更何況,我可能并不想只調用 notifyDatasetChanged() ,我有時想調用 notifyItemRemoved() ,或是 notifyItemChaned() 。
當然,你也可以給 adapter 加上一個 getItems() 的方法,然后把這些對 items 的操作邏輯都移動上層去處理,但我自己還是傾向于在 adapter 內封裝這些方法。
// MulitTypeAdapter.java public List<IItem> getItems() { return items; }
OK,至此,我們僅僅實現了 adapter 的 getItemCount() 和 getItemViewType() 方法,但是別著急。
接下來,就該處理難啃的的 onCreateViewHolder() 和 onBindViewHolder() 了,先來看 onCreateViewHolder() 吧。
在 onCreateViewHolder() 中,我們要根據 viewType 來生成不同的 ViewHolder,假設這里我們只顯示 ImageViewHolder 和 TextViewHolder。要顯示的 item 分別為 ImageItem 和 TextItem。我們先定義一個抽象基類 ItemViewHolder,代碼如下:
// ItemViewHolder.java public abstract class ItemViewHolder extends RecyclerView.ViewHolder { public ItemViewHolder(View itemView) { super(itemView); }
public abstract void bindTo(MulitTypeAdapter.IItem item);
}</code></pre>
分別實現 ImageViewHolder 和 TextViewHolder:
// ImageViewHolder.java public class ImageViewHolder extends ItemViewHolder { public ImageViewHolder(View itemView) { super(itemView); }
public void bindTo(MulitTypeAdapter.IItem item) { ImageItem imageItem = (ImageItem) item; // then do something }
}
// TextViewHolder.java public class TextViewHolder extends ItemViewHolder { public TextViewHolder(View itemView) { super(itemView); }
public void bindTo(MulitTypeAdapter.IItem item) { TextItem textItem = (TextItem) item; // then do something }
}</code></pre>
然后實現 onCreateViewHolder() :
// MulitTypeAdapter.java @Override public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(viewType, parent, false); if (viewType == R.layout.item_image) { return new ImageViewHolder(itemView); } else if (viewType == R.layout.item_text) { return new TextViewHolder(itemView); } return null; }
實現 onBindViewHolder() :
// MulitTypeAdapter.java @Override public void onBindViewHolder(ItemViewHolder holder, int position) { holder.bindTo(items.get(position)); }
可以看到, onBindViewHolder() 的實現也已經變得非常簡潔。那么就剩下 onCreateViewHolder() 了。一般來說,我們會把這一部分邏輯通過工廠方法來優化,代碼如下所示:
// ViewHolderFactory.java public class ViewHolderFactory { public static ItemViewHolder create(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(viewType, parent, false); switch (viewType) { case R.layout.item_image: return new ImageViewHolder(itemView); case R.layout.item_text: return new TextViewHolder(itemView); default: return null; } } }
那么 onCreateViewHolder 就可以同樣簡化成一行代碼,如下所示:
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return ViewHolderFactory.create(parent, viewType); }
到目前為止,我們所做的和其它開發者所做的優化并沒有什么不同,但是別著急,因為我們都還沒有用上 databinding。接下來我們看看 databinding 的表現,看它是如何消除手動創建多個 ViewHolder 的。
我們要把 ImageItem 顯示在 item_image.xml 上,把 TextItem 顯示在 item_text.xml 上,我們分別用 databinding 的方式實現這兩個 xml。在此之前,我們先來為 ImageItem 和 TextItem 填充一些數據。
借助 unsplash 提供的 url 讓 ImageItem 產生隨機圖片 (別忘了在 AndroidManifest.xml 中加上網絡訪問權限),用當前日期時間作為 TextItem 的內容。
// ImageItem.java public class ImageItem implements MulitTypeAdapter.IItem { @Override public int getType() { return R.layout.item_image; }
//////////////////////////////////////////////// public final String url; public ImageItem() { url = "https://unsplash.it/200/200?random&" + new Random().nextInt(40); }
}
// TextItem.java public class TextItem implements MulitTypeAdapter.IItem { @Override public int getType() { return R.layout.item_text; }
/////////////////////////////////////////// public final String content; public TextItem() { content = new Date().toString(); }
}</code></pre>
item_image.xml :
<layout> <data> <variable name="item" type="com.baurine.multitypeadaptertutorial.item.ImageItem"/> </data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:error="@{@drawable/ic_launcher}" app:imageUrl="@{item.url}" app:placeholder="@{@drawable/ic_launcher}"/> </LinearLayout>
</layout></code></pre>
其中 ImageView 的 imageUrl/error/placeholder 屬性是使用 強大的 BindingAdapter 實現的,代碼如下:
// BindingUtil.java public class BindingUtil { @BindingAdapter({"imageUrl", "error", "placeholder"}) public static void loadImage(ImageView imgView, String url, Drawable error, Drawable placeholder) { Glide.with(imgView.getContext()) .load(url) .error(error) .placeholder(placeholder) .into(imgView); } }
item_text.xml :
<layout> <data> <variable name="item" type="com.baurine.multitypeadaptertutorial.item.TextItem"/> </data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{item.content}"/> </LinearLayout>
</layout></code></pre>
在使用了 databinding 后,在創建 ViewHolder 時,ViewHolder 里需要保存就是不再是 itemView,而是 ViewDataBinding,每一個使用 <layout></layout> 形式的 xml 布局都會被 databinding 框架自動生成一個 ViewDataBinding 類的派生類,比如 item_image.xml 會生成 ItemImageBinding, item_text.xml 會生成 ItemTextBinding,而 ViewDataBinding 是它們的基類。因此我們改寫 ItemViewHolder/ImageViewHolder/TextViewHolder。
public abstract class ItemViewHolder extends RecyclerView.ViewHolder { protected final ViewDataBinding binding;
public ItemViewHolder(ViewDataBinding binding) { super(binding.getRoot()); this.binding = binding; } public abstract void bindTo(MulitTypeAdapter.IItem item);
}
public class ImageViewHolder extends ItemViewHolder { public ImageViewHolder(ViewDataBinding binding) { super(binding); }
public void bindTo(MulitTypeAdapter.IItem item) { ImageItem imageItem = (ImageItem) item; ((ItemImageBinding) binding).setItem(imageItem); }
}
public class TextViewHolder extends ItemViewHolder { public TextViewHolder(ViewDataBinding binding) { super(binding); }
public void bindTo(MulitTypeAdapter.IItem item) { TextItem textItem = (TextItem) item; ((ItemTextBinding) binding).setItem(textItem); }
}</code></pre>
此時,ViewHolderFactory 中的代碼是這樣的,我們要 inflate 得到 ViewDataBinding,如下所示:
public class ViewHolderFactory { public static ItemViewHolder create(ViewGroup parent, int viewType) { ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false); switch (viewType) { case R.layout.item_image: return new ImageViewHolder(binding); case R.layout.item_text: return new TextViewHolder(binding); default: return null; } } }
接下來,終于到了最關鍵最核心的一步,下面注意啦,我要開始變形啦。
在 ImageViewHolder 和 TextViewHolder 的 bindTo() 方法中,我們分別進行了兩次類型轉化,但是,實際上,ViewDataBinding 為我們提供了一個另外一個更通用的方法 setVariable(int variableId, Object obj) 來對 xml 中的變量進行賦值,注意,它的第二個參數是一個 Object。
比如,在 ImageViewHolder 中,我們持有了 item_image.xml 對應的 ItemImageBinding 實例對象,我們可以用自動生成的 setItem((ImageItem)item) 方法來進行賦值,也可以使用 setVariable(BR.item, item) 來進行賦值,因為這個 ViewDataBinding 實例知道,這個 xml 中 BR.item 對應的類型是 ImageItem,所以它會自動把 item 轉化成 ImageItem 類型。我們直接來看一下 ItemImageBinding 內部是怎么來實現 setVariable() :
public boolean setVariable(int variableId, Object variable) { switch(variableId) { case BR.item : setItem((com.baurine.multitypeadaptertutorial.item.ImageItem) variable); return true; } return false; }
可見,這個方法就是對各種 setXyz 方法的一層封裝。而因為這個方法是由基類 ViewDataBinding 定義的,根據 OOP 的多態特性, 我們直接調用基類的 setVariable() 方法即可,因此,ImageViewHolder 中的 bindTo() 方法就可以簡化成一行代碼:
public void bindTo(MulitTypeAdapter.IItem item) { binding.setVariable(BR.item, item); }
而對于 TextViewHolder 來說,也是一樣的。
如此一來,如果我們在不同的 item xml 中使用相同的 variable name,如上例中都使用了 name="item" ,那么 bindTo() 方法就可以統一成一種寫法了,如上面所示。
ImageViewHolder 和 TextViewHolder 從形式上已經是一樣的了,那我們就沒有必要實現多個 ViewHolder 了,統一用一個 ItemViewHolder 來實現,并在 setVariable() 后執行 binding.executePendingBindings() 來讓 UI 馬上變化:
public class ItemViewHolder extends RecyclerView.ViewHolder { private final ViewDataBinding binding;
public ItemViewHolder(ViewDataBinding binding) { super(binding.getRoot()); this.binding = binding; } public void bindTo(MulitTypeAdapter.IItem item) { binding.setVariable(BR.item, item); binding.executePendingBindings(); }
}</code></pre>
但是我們一定要理解的是,單一 ViewHolder 的背后,是由 databinding 框架生成的多個 ViewDataBinding。總體上來說,代碼量并沒有減少,但對于我們開發者來說,要寫的代碼和邏輯確是大大減少了。
此時,ViewHolderFactory 可以簡化成如下所示:
public class ViewHolderFactory { public static ItemViewHolder create(ViewGroup parent, int viewType) { ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false); return new ItemViewHolder(binding); } }
但是實際上,由于我們并不需要多個 ViewHolder 了,這個工廠類也就失去意義了,我們把 create() 這個方法移到 ItemViewHolder 中,刪除 ViewHolderFactory 類,并修改 adapter 的 onCreateViewHolder() 方法,如下所示:
// ItemViewHolder.java public static ItemViewHolder create(ViewGroup parent, int viewType) { ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false); return new ItemViewHolder(binding); }
// MulitTypeAdapter.java public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return ItemViewHolder.create(parent, viewType); }</code></pre>
更進一步,由于我們只有一個 ItemViewHolder,而且不需要對外公開,因此我們把它整體移入到 MulitTypeAdapter 類中,作為內部靜態類。至此,整個 adapter 全部完成
public class MultiTypeAdapter extends RecyclerView.Adapter<MultiTypeAdapter.ItemViewHolder> {
public interface IItem { // should directly return layout int getType(); } private List<IItem> items = new ArrayList<>(); /////////////////////////////////////////////////////// @Override public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return ItemViewHolder.create(parent, viewType); } @Override public void onBindViewHolder(ItemViewHolder holder, int position) { holder.bindTo(items.get(position)); } @Override public int getItemCount() { return items.size(); } @Override public int getItemViewType(int position) { return items.get(position).getType(); } /////////////////////////////////////////////////////// // operate items public List<IItem> getItems() { return items; } public void setItem(IItem item) { clearItems(); addItem(item); } public void setItems(List<IItem> items) { clearItems(); addItems(items); } public void addItem(IItem item) { items.add(item); } public void addItem(IItem item, int index) { items.add(index, item); } public void addItems(List<IItem> items) { this.items.addAll(items); } public void removeItem(IItem item) { items.remove(item); } public void clearItems() { items.clear(); } /////////////////////////////////////////////////// static class ItemViewHolder extends RecyclerView.ViewHolder { private final ViewDataBinding binding; static ItemViewHolder create(ViewGroup parent, int viewType) { ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false); return new ItemViewHolder(binding); } ItemViewHolder(ViewDataBinding binding) { super(binding.getRoot()); this.binding = binding; } void bindTo(MultiTypeAdapter.IItem item) { binding.setVariable(BR.item, item); binding.executePendingBindings(); } }
}</code></pre>
從此,我們就可以和 viewholder 說拜拜了,我們的重心轉移到實現一個又一個的 Item 上,而 Item 是極為輕量的。
至此,我們一步一步地實現了這個目前還不到 100 行的極簡 adapter,那如何使用它來,來輕松地實現 header, footer 呢,且聽下回 分解。
來自:https://github.com/baurine/multi-type-adapter/blob/master/note/multi-type-adapter-tutorial-1.md