借助 android databinding 框架,逃離 adapter 和 viewholder 的噩夢

借助 android databinding 框架,實現一個極簡的 recyclerview adapter,支持多類型。并且演示使用這個 adpater 實現 HeaderItem, 刷新,加載更多,重試的完整示例。 —— 由Baurine分享

前言

理解這篇文章最好有 databinding 的基礎知識,如果之前沒有了解過,推薦下面三篇文章:
官方文檔
connorlin 的中文翻譯
棉花糖給 Android 帶來的 Data Bindings

不多說,直接看一下最后的效果,UI 就別吐槽了,畢竟對于工程師來說,獨立開發一個應用,最大的問題就是選顏色了(其實是我啦),本來想用一個 GIF 來展示所有效果的,但一個 GIF 體積太大了,所以就拆成了三個:
演示 HeaderItem,EmptyItem,ErrorItem,刷新,加載更多,加載出錯并重試:

演示 沒有更多數據:

稍微潤色了一下 UI,為數據 Item 增加各種點擊事件:

這個完整的例子包括了以下功能,我想應該能滿足 90% 的需求吧:
刷新
加載更多
支持 header item
支持 empty item,并允許再次刷新
支持 error item,并允許再次刷新
支持 footer item,包括 3 種狀態:加載中,出錯并允許重試,沒有更多數據
支持多種數據類型的 item,我們在這個例子中只展示了 ImageItem 和 TextItem 兩種類型

下載 apk | 項目地址
這個項目中有兩個文件夾,MultiTyepAdapterSample 和 MultiTypeAdapterTutorial,兩者代碼是幾乎相同的,后者是我為了寫這篇文章重新創建的,每一個關鍵步驟我都打好了 tag,以方便讀者進行對照。
PS: 下面的內容完全是基于 databinding 思想來展開講的,所以如果你對它不感興趣,那么下面的內容對你就沒什么幫助。另外,文章比較長且啰索 (力爭講解到每一個細節),需要比較充裕的時間來閱讀和練習。如果你對 databinding 感興趣,歡迎提交 issue 探討。

大綱

實現篇
一步一步實現極簡的 adapter

使用篇
設置 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

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

推薦閱讀更多精彩內容