Android 複雜的列表視圖新寫法 MultiType (v3.1.0 修訂版)

前言

MultiType 這個項目,至今 v3.1.0 穩定多時,考慮得很是多,但也作得很是剋制。原則一直是 直觀、靈活、可靠、簡單純粹(其中直觀和靈活是很是看重的)。java

在開發個人 TimeMachine 時,我有一個複雜的聊天頁面,因而我設計了個人類型池系統,它是徹底解耦的,我可以輕鬆將它抽離出來分享,並給它取名爲 MultiType.android

從前,好比咱們寫一個相似微博列表頁面,這樣的列表是十分複雜的:有純文本的、帶轉發原文的、帶圖片的、帶視頻的、帶文章的等等,甚至穿插一條能夠橫向滑動的好友推薦條目。不一樣的 item 類型衆多,並且隨着業務發展,還會更多。若是咱們使用傳統的開發方式,常常要作一些繁瑣的工做,代碼可能都堆積在一個 Adapter 中:咱們須要覆寫 RecyclerView.AdaptergetItemViewType 方法,羅列一些 type 整型常量,而且 ViewHolder 轉型、綁定數據也比較麻煩。一旦產品需求有變,或者產品設計說須要增長一種新的 item 類型,咱們須要去代碼堆裏找到原來的邏輯去修改,或找到正確的位置去增長代碼。這些過程都比較繁瑣,侵入較強,須要當心翼翼,以避免改錯影響到其餘地方。git

如今好了,咱們有了 MultiType,簡單來講,MultiType 就是一個多類型列表視圖的中間分發框架,它能幫助你快速而且清晰地開發一些複雜的列表頁面,數據驅動視圖。 它本是爲聊天頁面開發的,聊天頁面的消息類型也是有大量不一樣種類,且新增頻繁,而 MultiType 可以輕鬆勝任。github

MultiType 以靈活直觀爲第一宗旨進行設計,它內建了 類型 - View 的複用池系統,支持 RecyclerView,隨時可拓展新的類型進入列表當中,使用簡單,令代碼清晰、模塊化、靈活可變。編程

所以,我寫了這篇文章,目的有幾個:一是以做者的角度對 MultiType 進行入門和進階詳解。二是傳遞我開發過程當中的思想、設計理念,這些偏細膩的內容,即便不使用 MultiType,想必也能帶來不少啓發。最後就是把自我以爲不錯的東西分享給你們,試想若是你製造的東西不少人在用,即便沒有帶來任何收益,也是一件很自豪的事情。json

目錄

MultiType 的特性

  • 輕盈,整個類庫只有 14 個類文件,aarjar 包大小隻有 13 KB
  • 周到,支持 data type <--> item view binder 之間 一對一 和 一對多 的關係綁定
  • 靈活,幾乎全部的部件(類)均可被替換、可繼承定製,面向接口 / 抽象編程
  • 純粹,只負責本分工做,專一多類型的列表視圖 類型分發,毫不會去影響 views 的內容或行爲
  • 高效,沒有性能損失,內存友好,最大限度發揮 RecyclerView 的複用性
  • 可讀,代碼清晰乾淨、設計精巧,極力避免複雜化,可讀性很好,爲拓展和自行解決問題提供了基礎

總覽

MultiType 能輕鬆實現以下頁面,它們將在示例篇章具體提供: 微信

MultiType 的源碼關係:數據結構

MultiType 基礎用法

可能有的新手看到以上特性介紹說什麼 "一對多"、抽象編程等等,都不太懂,我想說徹底沒關係,不懂能夠回過頭來再看,咱們先從基礎用法入手,其實 MultiType 使用起來特別簡單。使用 MultiType 通常狀況下只要 maven 引入 + 三個小步驟。以後還會介紹使用插件生成代碼方式,步驟將更加簡化:app

引入

在你的 build.gradle:框架

dependencies {
    compile 'me.drakeet.multitype:multitype:3.1.0'
}複製代碼

注:MultiType 內部引用了 recyclerview-v7:25.3.1,若是你不想使用這個版本,可使用 exclude 將它排除掉,再自行引入你選擇的版本。示例以下:

dependencies {
    compile('me.drakeet.multitype:multitype:3.1.0', {
       exclude group: 'com.android.support'
    })
    compile 'com.android.support:recyclerview-v7:你選擇的版本'
}複製代碼

Note: MultiType does not support RecyclerView below version 23.0.0.

使用

Step 1. 建立一個 class,它將是你的數據類型或 Java bean / model. 對這個類的內容沒有任何限制。示例以下:

public class Category {

    @NonNull public final String text;

    public Category(@NonNull String text) {
        this.text = text;
    }
}複製代碼

Step 2. 建立一個 class 繼承 ItemViewBinder.

ItemViewBinder 是個抽象類,其中 onCreateViewHolder 方法用於生產你的 item view holder, onBindViewHolder 用於綁定數據到 Views. 通常一個 ItemViewBinder 類在內存中只會有一個實例對象,MultiType 內部將複用這個 binder 對象來生產全部相關的 item views 和綁定數據。示例:

public class CategoryViewBinder extends ItemViewBinder<Category, CategoryViewBinder.ViewHolder> {

    @NonNull @Override
    protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        View root = inflater.inflate(R.layout.item_category, parent, false);
        return new ViewHolder(root);
    }

    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Category category) {
        holder.category.setText(category.text);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        @NonNull private final TextView category;

        ViewHolder(@NonNull View itemView) {
            super(itemView);
            this.category = (TextView) itemView.findViewById(R.id.category);
        }
    }
}複製代碼

Step 3. 在 Activity 中加入 RecyclerViewList 並註冊你的類型,示例:

public class MainActivity extends AppCompatActivity {

    private MultiTypeAdapter adapter;

    /* Items 等同於 ArrayList<Object> */
    private Items items;

    @Override 
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);
        /* 注意:咱們已經在 XML 佈局中經過 app:layoutManager="LinearLayoutManager" * 給這個 RecyclerView 指定了 LayoutManager,所以此處無需再設置 */

        adapter = new MultiTypeAdapter();

        /* 註冊類型和 View 的對應關係 */
        adapter.register(Category.class, new CategoryViewBinder());
        adapter.register(Song.class, new SongViewBinder());
        recyclerView.setAdapter(adapter);

        /* 模擬加載數據,也能夠稍後再加載,而後使用 * adapter.notifyDataSetChanged() 刷新列表 */
        items = new Items();
        for (int i = 0; i < 20; i++) {
            items.add(new Category("Songs"));
            items.add(new Song("drakeet", R.drawable.avatar_dakeet));
            items.add(new Song("許岑", R.drawable.avatar_cen));
        }
        adapter.setItems(items);
        adapter.notifyDataSetChanged();
    }
}複製代碼

大功告成!這就是 MultiType 的基礎用法了。其中 onCreateViewHolderonBindViewHolder 方法名沿襲了使用 RecyclerView 的習慣,使人一目瞭然,減小了新人的學習成本。

設計思想

MultiType 設計伊始,我給它定了幾個原則:

  • 要簡單,便於他人閱讀代碼

    所以我極力避免將它複雜化,避免加入許多不相干的內容。我想寫人人可讀的代碼,使用簡單精巧的方式,去實現複雜的需求。過多不相干、不必的代碼,將會使項目變得使人暈頭轉向,難以閱讀,遇到須要定製、解決問題的時候,無從下手。

  • 要靈活,便於拓展和適應各類需求

    不少人會得意地告訴我,他們把 MultiType 源碼精簡成三四個類,甚至一個類,覺得代碼越少就是越好,這我不能贊同。MultiType 考慮得更遠,這是一個提供給大衆使用的類庫,過分的精簡只會使得大幅失去靈活性。它或許不是使用起來最簡單的,但極可能是使用起來最靈活的,均衡性最好的。 在我看來,"直觀"、"靈活"優先級大於"簡單"。所以,MultiType 以接口或抽象進行鏈接,這意味着它的角色、組件均可以被替換,或者被拓展和繼承。若是你以爲它使用起來還不夠簡單,徹底能夠經過繼承封裝出更具體符合你使用需求的方法。它已經暴露了足夠豐富、周到的接口以供拓展,咱們不該該直接去修改源碼,這會致使一旦後續發現你的精簡版知足不了你的需求時,已經沒有回頭路了。

  • 要直觀,使用起來能令項目代碼更清晰可讀,一目瞭然

    MultiType 提供的 ItemViewBinder 沿襲了 RecyclerView Adapter 的接口命名,使用起來更加溫馨,符合習慣。另外,MultiType 不少地方放棄使用反射而是讓用戶顯式指明一些關係,如:MultiTypeAdapter#register 方法,須要傳遞一個數據模型 classItemViewBinder 對象,雖然有不少方法能夠把它精簡成單一參數方法,但咱們認爲顯式聲明數據模型類與對應關係,更加直觀。

高級用法

介紹了基礎用法和設計思想後,咱們能夠來介紹一下 MultiType 的高級用法。這是一些典型需求和案例,它們是基礎用法的延伸,也是設計思想的體現。也許一開始並不會使用到,但如若瞭解,可以拓寬使用 MultiType 的思路,也能過了解到咱們考慮問題的角度。

使用 MultiTypeTemplates 插件自動生成代碼

在基礎用法中,咱們了經過 3 個步驟完成 MultiType 的初次接入使用,實際上這個過程能夠更加簡化,MultiType 提供了 Android Studio 插件來自動生成代碼:

MultiTypeTemplates,源碼也是開源的,github.com/drakeet/Mul…。這個插件不只提供了一鍵生成 item 類文件和 ItemViewBinder,並且是一個很好的利用代碼模版自動生成代碼的示例。其中使用到了官方提供的代碼模版 API,也用到了我本身發明的更靈活修改模版內容的方法,有興趣作這方面插件的能夠看看。

話說回來,安裝和使用 MultiTypeTemplates 很是簡單:

Step 1. 打開 Android Studio 的設置 -> Plugin -> Browse repositories,搜索 MultiTypeTemplates 便可得到下載安裝:

Step 2. 安裝完成後,重啓 Android Studio. 右鍵點擊你的 package,選擇 New -> MultiType Item,而後輸入你的 item 名字,它就會自動生成 item 模型類 和 ItemViewBinder 文件和代碼。

好比你輸入的是 "Category",它就會自動生成 Category.javaCategoryViewBinder.java.

特別方便,相信你會很喜歡它。將來這個插件也將會支持自動生成佈局文件,這是目前欠缺的,但沒關係,其實 AS 在這方面已經很方便了,對佈局 R.layout.item_category 使用 alt + enter 快捷鍵便可自動生成佈局文件。

一個類型對應多個 ItemViewBinder

MultiType 支持一個類型對應多個 ItemViewBinder,獨創了具備良好 API 且高性能的一對多 Link 模型。使用方式也很簡單直觀,以下:

adapter.register(Data.class).to(
    new DataType1ViewBinder(),
    new DataType2ViewBinder()
).withClassLinker(new ClassLinker<Data>() {
    @NonNull @Override
    public Class<? extends ItemViewBinder<Data, ?>> index(@NonNull Data data) {
        if (data.type == Data.TYPE_2) {
            return DataType2ViewBinder.class;
        } else {
            return DataType1ViewBinder.class;
        }
    }
});複製代碼

或者:

adapter.register(Data.class).to(
    new DataType1ViewBinder(),
    new DataType2ViewBinder()
).withLinker(new Linker<Data>() {
    @Override
    public int index(@NonNull Data data) {
        return data.type == Data.TYPE_2 ? 1 : 0;
    }
});複製代碼

若是你使用 Lambda 表達式,以上代碼能夠更簡潔:

解釋:

如上示例代碼,對於一對多,咱們須要使用 MultiType#register(class) 方法,它會返回一個 OneToManyFlow 讓你緊接着綁定多個 ItemViewBinder 實例,最後再調用 OneToManyEndpoint#withLinkerOneToManyEndpoint#withClassLinker 操做符方法類設置 linker. 所謂 linker,是負責動態鏈接這個 "一" 對應 "多" 中哪個 binder 的角色。

這個方案具備很好的性能表現,並且可謂十分直觀。另外,我使用了 @CheckResult 註解來讓編譯器督促開發者必定要完整調用方法鏈纔不至於出錯。

更詳細的"一對多"示例能夠參考個人 sample 源碼:github.com/drakeet/Mul…

使用 全局類型池

MultiType 在 3.0 版本以前一直是支持全局類型池的,你能夠往一個全局類型池中 register 類型和 view binder,而後讓你的各個 MultiTypeAdapter 都能使用它。

但在 MultiType 3.0 以後,咱們廢棄並刪除了內置的全局類型池。緣由在於全局類型池容易對全局產生不可見影響,好比你註冊了一堆全局類型關係並在多處引用它,某一天你的夥伴不當心修改了全局類型池的某個內容,將致使全部使用的地方皆受到變化,是咱們不但願發生的。一個好的模塊,應該是高內聚、自包含的,若是過多下放權力到外圍,很容易遭受破壞或影響。

另外,全局類型池通常都是 static 形式的,若是咱們給這個 static 容器傳遞了 ActivityContext 對象,而沒有在退出時釋放,就容易造出內存泄漏,這對新手來講很容易觸犯。

所以咱們刪除了內置的全局類型池,當你建立一個 MultiTypeAdapter 對象時,默認狀況下,它內部會自動建立一個局部類型池以供你接下來註冊類型。固然了,若是你實在須要它,徹底能夠本身建立一個 static 的 MultiTypePool,而後經過 MultiTypeAdapter#registerAll(pool) 將這個類型池傳入,以此達到多個地方共同使用。

ItemViewBinder 通信

ItemViewBinder 對象能夠接受外部類型、回調函數,只要在使用以前,傳遞進去便可,例如:

OnClickListener listener = new OnClickListener() {
    @Override
    public void onClick(View v) {
        // ...
    }
}
adapter.register(Post.class, new PostViewBinder(xxx, listener));複製代碼

但話說回來,對於點擊事件,能不依賴 binder 外部內容的話,最好就在 binder 內部完成。binder 內部可以拿到 Views 和 數據,大部分狀況下,徹底有能力不依賴外部 獨立完成邏輯。這樣能使代碼更加模塊化,實現解耦和內聚。例以下面即是一個徹底自包含的例子:

public class SquareViewBinder extends ItemViewBinder<Square, SquareViewBinder.ViewHolder> {

    @NonNull @Override
    protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        View root = inflater.inflate(R.layout.item_square, parent, false);
        return new ViewHolder(root);
    }

    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Square square) {
        holder.square = square;
        holder.squareView.setText(valueOf(square.number));
        holder.squareView.setSelected(square.isSelected);
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        private TextView squareView;
        private Square square;

        ViewHolder(final View itemView) {
            super(itemView);
            squareView = (TextView) itemView.findViewById(R.id.square);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override 
                public void onClick(View v) {
                    itemView.setSelected(square.isSelected = !square.isSelected);
                }
            });
        }
    }
}複製代碼

使用斷言,比傳統 Adapter 更加易於調試

衆所周知,若是一個傳統的 RecyclerView Adapter 內部有異常致使崩潰,它的異常棧是不會指向到你的 Activity,這給咱們開發調試過程當中帶來了麻煩。若是咱們的 Adapter 是複用的,就不知道是哪個頁面崩潰。而對於 MultiTypeAdapter,咱們顯然要用於多個地方,並且可能出現開發者忘記註冊類型等等問題。爲了便於調試,開發期快速失敗,MultiType 提供了很方便的斷言 API: MultiTypeAsserts,使用方式以下:

import static me.drakeet.multitype.MultiTypeAsserts.assertAllRegistered;
import static me.drakeet.multitype.MultiTypeAsserts.assertHasTheSameAdapter;

public class SimpleActivity extends MenuBaseActivity {

    private Items items;
    private MultiTypeAdapter adapter;

    @Override 
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);

        items = new Items();
        adapter = new MultiTypeAdapter(items);
        adapter.register(TextItem.class, new TextItemViewBinder());

        for (int i = 0; i < 20; i++) {
            items.add(new TextItem(valueOf(i)));
        }

        /* 斷言全部使用的類型都已註冊 */
        assertAllRegistered(adapter, items);
        recyclerView.setAdapter(adapter);
        /* 斷言 recyclerView 使用的是正確的 adapter */
        assertHasTheSameAdapter(recyclerView, adapter);
    }
}複製代碼

assertAllRegisteredassertHasTheSameAdapter 都是可選擇性使用,assertAllRegistered 須要在加載或更新數據以後, assertHasTheSameAdapter 必須在 recyclerView.setAdapter(adapter) 以後。

這樣作之後,MultiTypeAdapter 相關的異常都會報到你的 Activity,而且會詳細註明出錯的緣由,而若是符合斷言,斷言代碼不會有任何反作用或影響你的代碼邏輯,這時你能夠把它看成廢話。關於這個類的源代碼是很簡單的,有興趣能夠直接看看源碼:drakeet/multitype/MultiTypeAsserts.java

支持 Google AutoValue

AutoValue 是 Google 提供的一個在 Java 實體類中自動生成代碼的類庫,使你更專一於處理項目的其餘邏輯,它可以使代碼更少,更乾淨,以及更少的 bug.

當咱們使用傳統方式建立一個 Java 模型類的時候,常常須要寫一堆 toString()hashCode()、getter、setter 等等方法,並且對於 Android 開發,大多狀況下還須要實現 Parcelable 接口。這樣的結果是,我原本想要一個只有幾個屬性的小模型類,但出於各類緣由,這個模型類方法數變得十分繁複,閱讀起來很不清爽,而且不免會寫錯內容。AutoValue 的出現解決了這個問題,咱們只需定義一些抽象類交給 AutoValue,AutoValue 會自動生成該抽象類的具體實現子類,並攜帶各類樣板代碼。

更詳細的介紹內容和使用教程,我會在文章末尾會給出 AutoValue 的相關連接,不熟悉 AutoValue 能夠藉此機會看一下,在這裏就不作過多介紹了。新手暫時看不懂也沒必要糾結,瞭解以後都是十分容易的。

MultiType 支持了 Google AutoValue,支持自動映射某個已經註冊的類型的子類到同一 ItemViewBinder,規則是:若是子類註冊,就用註冊的映射關係;若是子類註冊,則該子類對象使用註冊過的父類映射關係。

FlatTypeAdapter(已廢棄)

MultiType 3.0 以前提供了一個 FlatTypeAdapter 類,3.0 以後,這個類已經被刪除了,你能夠徹底沒必要關心它。若是你使用過它,如今它已經被一對多方案替代了,請轉成使用一對多功能實現。

MultiType 與下拉刷新、加載更多、HeaderView、FooterView、Diff

MultiType 設計從始至終,都極力避免往復雜化方向發展,一開始個人設計宗旨就是它應該是一個很是純粹的、專注的項目,而非各類亂七八糟的功能都要囊括進來的多合一大型庫,所以它很剋制,期間有許多人給我發過一些無關特性的 Pull Request,表示感謝,但全被拒絕了。

對於不少人關心的 下拉刷新、加載更多、HeaderView、FooterView、Diff 這些功能特性,其實都不該該是 MultiType 的範疇,MultiType 的份內之事是作類型、事件與 View 的分發、鏈接工做,其他無關的需求,都是能夠在 MultiType 外部完成,或者經過繼承 進行自行封裝和拓展,而做爲一個基礎、公共類庫,我想它是不該該包含這些內容。

但不少新手可能並不習慣代碼分工、模塊化,所以在此我有必要對這幾個點簡單示範下如何在 MultiType 以外去實現:

  • 下拉刷新:

    對於下拉刷新,Android 官方提供了 support.v4 SwipeRefreshLayout,在 Activity 層面,能夠拿到 SwipeRefreshLayout 調用 setOnRefreshListener 設置監聽器便可.

    或者參考個人 rebase-android 項目編寫的 SwipeRefreshDelegate.java.

  • 加載更多:

    RecyclerView 提供了 addOnScrollListener 滾動位置變化監聽,要實現加載更多,只要監聽並檢測列表是否滾動到底部便可,有多種方式,鑑於 LayoutManager 本應該只作佈局相關的事務,所以咱們推薦直接在 OnScrollListener 層面進行判斷。提供一個簡單版 OnScrollListener 繼承類:

    public abstract class OnLoadMoreListener extends RecyclerView.OnScrollListener {
    
        private LinearLayoutManager layoutManager;
        private int itemCount, lastPosition, lastItemCount;
    
        public abstract void onLoadMore();
    
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
                layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
    
                itemCount = layoutManager.getItemCount();
                lastPosition = layoutManager.findLastCompletelyVisibleItemPosition();
            } else {
                Log.e("OnLoadMoreListener", "The OnLoadMoreListener only support LinearLayoutManager");
                return;
            }
    
            if (lastItemCount != itemCount && lastPosition == itemCount - 1) {
                lastItemCount = itemCount;
                this.onLoadMore();
            }
        }
    }複製代碼

    或者參考個人 rebase-android 項目編寫的 LoadMoreDelegate.java.

  • 獲取數據後作 Diff 更新:

    MultiType 支持 onBindViewHolder with payloads,詳情見 ItemViewBinder 類文檔。對於 Diff,能夠在 Activity 中進行 Diff,或者繼承 MultiTypeAdapter 提供接收數據方法,在方法中進行 Diff. MultiType 不提供內置 Diff 方案,否則須要依賴 v4 包,而且這也不該該屬於它的範疇。

    示例代碼:github.com/drakeet/Mul…

  • HeaderView、FooterView

    MultiType 其實自己就支持 HeaderViewFooterView,只要建立一個 Header.class - HeaderViewBinderFooter.class - FooterViewBinder 便可,而後把 new Header() 添加到 items 第一個位置,把 new Footer() 添加到 items 最後一個位置。須要注意的是,若是使用了 Footer View,在底部插入數據的時候,須要添加到 最後位置 - 1,即倒二個位置,或者把 Footer remove 掉,再添加數據,最後再插入一個新的 Footer.

實現 RecyclerView 嵌套橫向 RecyclerView

MultiType 天生就適合實現相似 Google Play 或 iOS App Store 那樣複雜的首頁列表,這種頁面一般會在垂直列表中嵌套橫向列表,其實橫向列表咱們徹底能夠把它視爲一種 Item 類型,這個 item 持有一個列表數據和當前橫向列表滑動到的位置,相似這樣:

public class PostList {

    public final List<Post> posts;
    public int currentPosition;

    public PostList(@NonNull List<Post> posts) { this.posts = posts; }
}複製代碼

對應的 HorizontalItemViewBinder 相似這樣:

public class HorizontalItemViewBinder extends ItemViewBinder<PostList, HorizontalItemViewBinder.ViewHolder> {

    @NonNull @Override
    protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        /* item_horizontal_list 就是一個只有 RecyclerView 的佈局 */
        View view = inflater.inflate(R.layout.item_horizontal_list, parent, false);
        return new ViewHolder(view);
    }

    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull PostList postList) {
        holder.setPosts(postList.posts);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        private RecyclerView recyclerView;
        private PostsAdapter adapter;

        private ViewHolder(@NonNull View itemView) {
            super(itemView);
            recyclerView = (RecyclerView) itemView.findViewById(R.id.post_list);
            LinearLayoutManager layoutManager = new LinearLayoutManager(itemView.getContext());
            layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
            recyclerView.setLayoutManager(layoutManager);
            /* adapter 只負責灌輸、適配數據,佈局交給 LayoutManager,可複用 */
            adapter = new PostsAdapter();    // 或者直接使用 MultiTypeAdapter 更加方便
            recyclerView.setAdapter(adapter);
            /* 在此設置橫向滑動監聽器,用於記錄和恢復當前滑動到的位置,略 */
            ...
        }

        private void setPosts(List<Post> posts) {
            adapter.setPosts(posts);
            adapter.notifyDataSetChanged();
        }
    }
}複製代碼

實現線性佈局和網格佈局混排列表

這個課題其實也不屬於 MultiType 的範疇,MultiType 的職責是作數據類型分發,而不是佈局,但鑑於不少複雜頁面都會須要線性佈局和網格佈局混排,我就簡單講一講,關鍵在於 RecyclerViewLayoutManager. 雖然是線性和網格混合,但實現起來其實只要一個網格佈局 GridLayoutManager,若是你查看 GridLayoutManager 的官方源碼,你會發現它其實繼承自 LinearLayoutManager. 如下是示例和解釋:

public class MultiGridActivity extends MenuBaseActivity {

    private final static int SPAN_COUNT = 5;
    private MultiTypeAdapter adapter;
    private Items items;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multi_grid);
        items = new Items();
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);

        final GridLayoutManager layoutManager = new GridLayoutManager(this, SPAN_COUNT);

        /* 關鍵內容:經過 setSpanSizeLookup 來告訴佈局,你的 item 佔幾個橫向單位, * 若是你橫向有 5 個單位,而你返回當前 item 佔用 5 個單位,那麼它就會看起來單獨佔用一行 */
        layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return (items.get(position) instanceof Category) ? SPAN_COUNT : 1;
            }
        });
        recyclerView.setLayoutManager(layoutManager);

        adapter = new MultiTypeAdapter(items);
        adapter.applyGlobalMultiTypePool();
        adapter.register(Square.class, new SquareViewBinder());

        assertAllRegistered(adapter, items);
        recyclerView.setAdapter(adapter);
        loadData();
    }

    private void loadData() {
        // ...
    }
}複製代碼

數據扁平化處理

在一個垂直 RecyclerView 中,item 們都是同級的,沒有任何嵌套關係,但咱們的數據結構每每存在嵌套關係,好比 Post 內部包含了 Comments 數據,或換句話說 Post 嵌套了 Comment,就像微信朋友圈同樣,"動態" 伴隨着 "評論"。那麼如何把 非扁平化 的數據排布在 扁平 的列表中呢?必然須要一個數據扁平化處理的過程,就像 ListView 的數據須要一個 Adapter 來適配,Adapter 就像一個油漏斗,把油引入瓶子中。咱們在面對嵌套數據結構的時候,能夠採用以下的扁平化處理,關於扁平化這個詞,沒必要太糾結,簡單說,就是把嵌套數據都拉出來,攤平,讓 CommentPost 同級,最後把它們都 add 進同一個 Items 容器,交給 MultiTypeAdapter. 示例:

假設:你的 Post 是這樣的:

public class Post {

    public String content;
    public List<Comment> comments; 
}複製代碼

假設:你的 Comment 是這樣的:

public class Comment {

    public String content;
}複製代碼

假設:你服務端返回的 JSON 數據是這樣的:

[
    {
        "content":"I have released the MultiType v2.2.2", 
        "comments":[
            {"content":"great"},
            {"content":"I love your post!"}
        ]
    }
]複製代碼

那麼你的 JSON 轉成 Java Bean 以後,你拿到手應該是個 List<Post> posts 對象,如今咱們寫一個扁平化處理的方法:

private List<Object> flattenData(List<Post> posts) {
    final List<Object> items = new ArrayList<>();
    for (Post post : posts) {
        /* 將 post 加進 items,Binder 內部拿到它的時候, * 咱們無視它的 comments 內容便可 */
        items.add(post);
        /* 緊接着將 comments 拿出來插入進 items, * 評論就能正好處於該條 post 下面 */
        items.addAll(post.comments);
    }
    return items;
}複製代碼

最後咱們全部的 posts 在加入全局 MultiType Items 以前,都須要通過扁平化處理:

items.addAll(flattenData(posts));
adapter.notifyDataSetChanged();複製代碼

整個過程其實並不困難,相信你們都已經理解了。

更多示例

MultiType 的開源項目提供了許多的 samples (示例) 程序,這些示例秉承了一向的代碼清晰、乾淨的風格,十分易於閱讀:

  • 仿造微博的數據結構和二級 ItemViewBinder

    這是一個相似微博數據結構的示例,數據兩層結構,Item 也是兩層結構:一層框架(包含頭像用戶名等),一層 content view(微博內容),內容嵌套於框架中。微博的每一條微博 item 都包含了這樣兩層嵌套關係,這樣作的好處是,你沒必要每一個 item 都去重複製造一遍外層框架。

    或者換一個比喻,就像聊天消息,一條聊天消息也是兩層的,一層頭像、用戶名、聊天氣泡框,一層你的文字、圖片等。另外,每一種消息都有左邊和右邊的樣式,分別對應別人發來的消息和你發出的消息。若是左邊算一種,右邊又算一種,就是比較很差的設計了,會致使佈局內容重複、冗餘,修改操做都要作兩遍。最好的方案是讓他們視被爲同一種類型,而後在 item 框層次進行左右邊判斷和框架相關數據綁定。

    我提供的這個二級 ItemViewBinder 示例即是這樣的兩層結構。它可以讓你每次新增長一個類型,只要實現內容便可,框不該該重複實現。

    若是再不明白,或許你能夠看看個人這個示例中 微博 Item 框的佈局:

    從我這個 frame 佈局能夠看出來,它內部有一個 FrameLayout 做爲 container 將用於容納不一樣的微博內容,而這一層框架則是共同的。

    這個例子算高級中的高級,但實際上也是很簡單,展現了 MultiType 優秀的可拓展能力。完整運行結果展現以下:

    注:以上咱們並無提到服務端 JSON 數據轉爲咱們定義的 Weibo 對象過程,實際上對於完整鏈路,這個過程是須要作數據轉換,咱們須要在 WeiboContent 層加一個 typedescribe 字段用於描述微博內容類型,而後再將微博內容的 JSON 文本轉爲具體微博內容對象交給 Weibo. 這個內容建議直接閱讀這個 sample 的 WeiboContentDeserializer 源碼,我利用了一種很簡單又巧妙的方式,在 JSON 解析底層便進行抽象數據具體化,使得客戶端和服務端都可以輕鬆適應這種微博和微博內容嵌套關係。

  • drakeet/about-page

    一個 Material Design 的關於頁面,核心基於 MultiType,包含了多種 items,美觀,容易使用。

  • 線性和網格佈局混排

    使用 MultiTypeGridLayoutManager 實現網格和線性混合佈局,實現一個選集頁面。

  • drakeet/TimeMachine

    TimeMachine 使用了 MultiType 來建立一個複雜的聊天頁面,頁面和需求雖然複雜,但使用 MultiType 顯得輕鬆簡單。

  • 相似 Bilibili iOS 端首頁

    使用 MultiType 實現相似 Bilibili iOS 端首頁複雜的多類型列表視圖,包括嵌套橫向 RecyclerView.

Q & A

  • Q: 以爲 MultiType 不夠精簡,應該怎麼作?

    A: 在前面 "設計思想" 中咱們談到:MultiType 或許不是使用起來最簡單的,但極可能是使用起來最靈活的。其中的原因是它高度可定製、可拓展,而不是把一些路封死。做爲一個基礎類庫,簡單和靈活須要一個均衡點,過分精簡便要以失去靈活性爲代價。若是以爲 MultiType 不夠精簡,想將它修改得更加容易使用,我推薦的方式是去繼承 MultiTypeAdapterItemViewBinder,甚至你能夠從新實現一個 TypePool 再設置給 MultiTypeAdapter. 咱們不該該直接到底層去修改、破壞它們。總之,利用開放接口或繼承的作法無論對於 MultiType 仍是其它開源庫,都應該是定製的首選。

  • Q: 在 ItemViewBinder 中如何拿到 Context 對象?

    A: 有人問我說,他在 ItemViewBinder 裏使用 Glide 來加載圖片須要獲取到 Activity Context 對象,要怎麼才能拿到 Context 對象?這是一個特別簡單的問題,但我想既然有人問,應該比較典型,我就詳細解答下:首先,在 Android 開發中,任何 View 對象都能經過 view.getContext() 拿到 Context 對象,若是你須要經過 View 拿到 Activity 對象,而且你使用了 AppCompatActivity,那麼建議你這麼作:

    public class Activities {
    
        public static Activity getActivity(View view) {
            Context context = view.getContext();
            while (context instanceof ContextWrapper) {
                if (context instanceof Activity) {
                    return (Activity) context;
                }
                context = ((ContextWrapper) context).getBaseContext();
            }
            return null;
        }
    }
    
    Glide.with(getActivity(view)) ...複製代碼

    緣由及相關 issue:github.com/drakeet/Mul…

    總而言之,拿到 Context 對象很是簡單,只要你能拿到一個 View 對象,調用 view.getContext() 便可。另外,也能夠參考 與 binder 通信 章節,咱們能夠很方便地給 binder 傳遞任何對象進去,包括 Context 對象。

  • Q:如何在 ItemViewBinder 中獲取到 item position?

    A: 從 v2.3.5 版本開始,只須要在你的 ItemViewBinder 子類裏調用 getPosition(holder) 方法便可。另外,ItemViewBinder 還提供了 getAdapter() 或許也是不少人想要的,好比調用 adapter 進行 notify 刷新視圖等。

感謝

MultiType 開發維護過程當中,不少朋友給了我不少反饋,我也很是樂意於與你們交流,有問必答,由於這是一個可貴不錯的項目,它比較接近我心中對於一個完美項目的要求:設計精巧,代碼乾淨漂亮。

我向來是不太在乎項目的 star 數目的,但熱衷於把個人好東西分享給更多人使用,所以在個人 GitHub 首頁我不會把我一些高 star 項目擺出來,而是放一些我以爲代碼相對比較好的項目。這是個人動力,我想寫一份完美的代碼,就像王垠的 40 行同樣,達到自以爲完美無缺、猶如天神衣袖般的優雅,嗯,要是哪天我作到了,我就中止開源,哈哈。

話說回來,這個項目,特別感謝你們的幫忙、反饋,感謝一些朋友的 PR、貢獻和推薦,是大家讓我以爲開源是一件除了完善自我以外 還充滿了意義的一件事情 -- 可以與更多人協同,可以面向更寬廣的世界,謝謝你們!如下是感謝名單:

70kgzubinxiongWanLiLi代碼家CaMnterandroid-xiaoweiburgessjplixi0912simidaxu咕咚LuckyJayceBelongsHtmexceptTellHRay PanZackChris

引用文獻

相關文章
相關標籤/搜索