借助 android databinding 框架,實現一個極簡的 recyclerview adapter,支持多類型。并且演示使用這個 adpater 實現 HeaderItem, 刷新,加載更多,重試的完整示例。 —— 由Baurine分享
前言
理解這篇文章最好有 databinding 的基礎知識,如果之前沒有了解過,推薦下面三篇文章:
官方文檔
connorlin 的中文翻譯
棉花糖給 Android 帶來的 Data Bindings
不多說,直接看一下最后的效果,UI 就別吐槽了,畢竟對于工程師來說,獨立開發一個應用,最大的問題就是選顏色了(其實是我啦),本來想用一個 GIF 來展示所有效果的,但一個 GIF 體積太大了,所以就拆成了三個:
演示 HeaderItem,EmptyItem,ErrorItem,刷新,加載更多,加載出錯并重試:
這個完整的例子包括了以下功能,我想應該能滿足 90% 的需求吧:
刷新
加載更多
支持 header item
支持 empty item,并允許再次刷新
支持 error item,并允許再次刷新
支持 footer item,包括 3 種狀態:加載中,出錯并允許重試,沒有更多數據
支持多種數據類型的 item,我們在這個例子中只展示了 ImageItem 和 TextItem 兩種類型
下載 apk | 項目地址
這個項目中有兩個文件夾,MultiTyepAdapterSample 和 MultiTypeAdapterTutorial,兩者代碼是幾乎相同的,后者是我為了寫這篇文章重新創建的,每一個關鍵步驟我都打好了 tag,以方便讀者進行對照。
PS: 下面的內容完全是基于 databinding 思想來展開講的,所以如果你對它不感興趣,那么下面的內容對你就沒什么幫助。另外,文章比較長且啰索 (力爭講解到每一個細節),需要比較充裕的時間來閱讀和練習。如果你對 databinding 感興趣,歡迎提交 issue 探討。
大綱
使用篇
設置 RecyclerView 和 SwipeRefreshLayout
實現各種狀態類 item
實現刷新
實現加載更多
為 item 增加事件處理
獲取 item 的 position
item 與 model 的關系
優化篇
將 adapter 獨立成庫
使用 MVP 簡化 Activity 的邏輯
實現篇
一步一步實現極簡的 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'
}
接著,我們來開始來實現這個 adapter,取名為 MultiTypeAdapter,因為我們要支持多類型,那么 adapter 里的 item 必然是抽象的,我們定義為 IItem:
// MultiTypeAdapter.java
public interface IItem {
}
private List<IItem> items = new ArrayList<>();
我們先從簡單的入手,先來看看 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();
}
}
(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;
}
}
這樣,我們的 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();
}
你可能會想,誒,在這些操作函數里最后再加上 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()方法,但是別著急。
截止目前為止的代碼:tutorial_step_1
接下來,就該處理難啃的的 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);
}
分別實現 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
}
}
然后實現 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);
}
截止目前為止的代碼:tutorial_step_2
到目前為止,我們所做的和其它開發者所做的優化并沒有什么不同,但是別著急,因為我們都還沒有用上 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();
}
}
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>
其中 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>
在使用了 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);
}
}
此時,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;
}
}
}
截止目前為止的代碼:tutorial_step_3
接下來,終于到了最關鍵最核心的一步,下面注意啦,我要開始變形啦。
在 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();
}
}
但是我們一定要理解的是,單一 ViewHolder 的背后,是由 databinding 框架生成的多個 ViewDataBinding??傮w上來說,代碼量并沒有減少,但對于我們開發者來說,要寫的代碼和邏輯確是大大減少了。
此時,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);
}
更進一步,由于我們只有一個 ItemViewHolder,而且不需要對外公開,因此我們把它整體移入到 MulitTypeAdapter 類中,作為內部靜態類。至此,整個 adapter 全部完成,全部代碼如下所示 (tutorial_step_4
):
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();
}
}
}
從此,我們就可以和 viewholder 說拜拜了,我們的重心轉移到實現一個又一個的 Item 上,而 Item 是極為輕量的。
至此,我們一步一步地實現了這個目前還不到 100 行的極簡 adapter,那如何使用它來,來輕松地實現 header, footer 呢,且聽 下回 分解。
參考:優雅的實現多類型列表的Adapter
原文鏈接:https://github.com/baurine/multi-type-adapter/blob/master/note/multi-type-adapter-tutorial-1.md