真香警告!RecyclerView的新元件來了~

語言: CN / TW / HK

 BATcoder技術 群,讓一部分人先進大廠

大家 ,我是劉望舒,騰訊TVP,著有三本業內知名暢銷書,連續四年蟬聯電子工業出版社年度優秀作者,百度百科收錄的資深技術專家。

前華為架構師,現大廠技術總監。

想要 加入  BATcoder技術群,公號回覆 BAT  即可。

作者:瓊娣和予,原文連結: https://www.jianshu.com/p/67b0a3c3c2b3

前言

ConcatAdapter 是RecyclerView是在1.2.0版本上推出的新元件。今天主要介紹 ConcatAdapter 的基本使用和實現原理,包括:

  1. 使用特點

  2. 基礎用法

  3. 實現原理

  4. 核心處理邏輯:ConcatAdapter層、ConcatAdapterController層、Helper層

使用特點

RecyclerView 在使用Adapter載入資料的時候,可能會區分多種ViewType,一般的處理方式都是通過重新 AdaptergetItemViewType 方法返回不同的ViewType,然後在 onCreateViewHolder 方法裡面通過不同的ViewType來定義不同的佈局。

從一個具體的場景來說,RecyclerView通常會被分為三個部分,分別是:Header部分,Content部分,Footer部分,這其中三個部分的佈局均不相同,所以就需要通過不同的ViewType來實現目的。

不過,一般專案裡面會通過這種場景的邏輯均是通用的,所以我們最好是能將上面的邏輯定義成通用的從而方便使用。 ConcatAdapter 可以將幾個Adapter組合成為一個Adapter,每個子Adapter裡面的ViewType是相同,子Adapter之間的ViewType可以是不同的,這樣便能將不同的邏輯拆分,後續在複用起來就會更加的方便。

基礎使用

按照RecyclerView常見的Header、Content和Footer三個部分的場景,實現一個小小的Demo,具體效果如下圖:

  1. 先定義三個Adapter,用來載入不同部分的佈局:HeaderAdapter、ContentAdapter 和FooterAdapter

  2. 使用ConcatAdapter將三個Adapter組合起來。組合步驟主要分為如下2步:

a. 定義ConcatAdapter的Config。主要是配置ViewType是否相互隔離,以及stableId的策略。b. 使用ConcatAdapter將子Adapter組合起來。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 1. 定義Config
        val config = ConcatAdapter.Config.Builder()
            .setIsolateViewTypes(true)
            .setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
            .build()
        val adapter = ConcatAdapter(config)
        // 2. 使用ConcatAdapter將三個Adapter組合起來。
        adapter.addAdapter(HeaderAdapter(generateList("Header", 2)).apply { setHasStableIds(true)})
        adapter.addAdapter(ContentAdapter(generateList("Content", 2)).apply { setHasStableIds(true)})
        adapter.addAdapter(FooterAdapter(generateList("Footer", 2)).apply { setHasStableIds(true) })
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
    }

    private fun generateList(title: String, count: Int) = ArrayList<String>().apply {
        for (index in 0 until count) {
            add("$title position = $index")
        }
    }
}

ConcatAdapter 的基本使用過程就是如上所說的內容,但是其中還有很多隱藏細節我並沒有,比如說在定義Config的時候, setIsolateViewTypessetStableIdMode 這兩個方法的作用是什麼,以及ConcatAdapter究竟是怎麼將子Adapter串聯起來的。這些問題的答案,我都會在後面的內容詳細介紹。

實現原理

1. 執行流程

2. 組成

ConcatAdappter 的實現主要分為3層:

  1. ConcatAdapter層: ConcatAdappter 實現了 RecyclerView#Adapter 的很多方法,它主要是面對於RecyclerView。
  2. ConcatAdapterController
    ConcatAdapter
    ConcatAdapter
    onCreateViewHolder
    onBindViewHolder
    getItemCount
    ConcatAdapter
    ConcatAdapterController
    
  3. ConcatAdapterController
    ViewTypeStorage
    StableIdStorage
    stableId
    ConcatAdapterController
    

下面,將分別介紹這三層核心處理邏輯。

第一層:ConcatAdapter

其內部的實現跟我們平時使用的Adapter沒有多大的差別,不同的地方就在於 ConcatAdapter 將實現邏輯放在了 ConcatAdapterController 裡面的。需要特別說明的是:

  1. Config
    Config
    Config#DEFAULT
    Config
    
  2. ConcatAdapter
    setHasStableIds
    Config
    StableIdMode
    ISOLATED_STABLE_IDS
    SHARED_STABLE_IDS
    
  3. ConcatAdapter
    setStateRestorationPolicy
    setStateRestorationPolicy
    setStateRestorationPolicy
    RecyclerView
    setStateRestorationPolicy
    StateRestorationPolicy
    

第二層:ConcatAdapterController

ConcatAdapterController 內部,每個子Adapter都會被封裝成為一個 NestedAdapterWrapper 類,所以 ConcatAdapterController 的所有回撥方法都是直接或者間接通過呼叫 NestedAdapterWrapper 對應的方法實現邏輯。而每個方法裡面所需要的 NestedAdapterWrapper 物件都是通過兩種方式來獲取的:

ConcatAdapterController 的快取陣列 mWrappers 獲取:當我們呼叫addAdapter方法,會將每個Adapter包裝成為一個 NestedAdapterWrapper 物件,同時會將這個物件新增到 mWrappers 數組裡面去,可以通過position其他地方直接獲取。

ViewTypeStorage 裡面獲取: ViewTypeStorage 會通過不同的ViewType快取不同的 NestedAdapterWrapper ,可以通過ViewType來獲取。

區分一下 ConcatAdapterController 所有需要 NestedAdapterWrapper 物件的方法,只有 onCreateViewHolder 方法是通過方式2獲取的,其他方法都是通過方式1獲取的。正因為如此差別,就會出現一個特別的現象就是,一個子Adapter的onBindViewHolder方法裡面帶ViewHolder,不一定是自己的onCreateViewHolder方法建立,因為 ConcatAdapterControlleronCreateViewHolder 方法裡面和 onBindViewHolder 方法裡面使用的 NestedAdapterWrapper 物件不一定是同一個。 「這一點大家一定要特別注意」 。具體是什麼情況下才會出現物件不一樣的問題,這個在分析ViewType的時候會重點介紹。

ConcatAdapterController 除了將 ConcatAdapter 的回撥分發到每個子Adapter裡面,還有一個作用就是將每個子Adapter資料變換的通知同步到 ConcatAdapter 裡面去,因為從類圖上來看, ConcatAdapterController 實現了 NestedAdapterWrapper.Callback 介面,每個子Adapter都會通過該介面來通知資料變化的資訊。

第三層:Helper層

此處主要關注的是其核心處理邏輯:ViewType的處理策略、StableId的處理策略。

1. ViewType的處理策略

將多個Adapter組合到一個Adapter裡面,我們需要考慮一個問題,就是如果子Adapter有可能返回相同的ViewType,面對這種情況, ConcatAdapter 應該讓哪個子Adapter來建立ViewHolder呢?這是一個非常重要的。我們先來看一下Config針對於ViewType已有處理策略,即 isolateViewTypes 不同取值的含義。

取值 含義
true 表示子Adapter相互隔離ViewType,互不影響。比如說有兩個Adapter返回相同的ViewType,
那麼還是自己處理自己的,在onBindViewHolder方法裡面使用的ViewHolder,肯定是自己的
onCreateViewHolder方法創建出來的。
false 表示所有的子Adapter共享ViewType,以及共享ViewHolder。比如說Adapter A和Adapter B
返回了相同的ViewType,在Adapter A onBindViewHolder 方法裡面的ViewHolder有可能是
Adapter B的onCreateViewHolder出來的。

我們在使用的時候,可以直接通過設定這個欄位的值,以達到不同的目的。但是有沒有思考過,ConcatAdapter是怎麼處理的呢?接下來我們將正式這兩種策略的實現原理,不過在這之前我們來了解一下實現ViewType處理策略整體結構。

1.1 ViewTypeStorage

ConcatAdapterContrller在處理ViewType時,會根據我們 isolateViewTypes 不同取值建立不同 ViewTypeStorage 物件,我們先來看一下這個介面的結構: 我分別解釋一下這兩個方法,含義如下表:

方法名 含義
getWrapperForGlobalType 該方法的作用是通過傳入進入的ViewType獲取一個
NestedAdapterWrapper 物件。 ConcatAdapterContrller
onCreateViewHolder方法就是通過該方法獲取獲取的
NestedAdapterWrapper
createViewTypeWrapper 該方法的作用是通過傳入的進來 NestedAdapterWrapper
的物件,建立一個 ViewTypeLookup 物件。這其中,
ViewTypeLookup 會將傳入進來的 NestedAdapterWrapper
物件快取起來,方便 getWrapperForGlobalType 方法通
過ViewType獲取。

大家可能會對 ViewTypeLookup 有疑惑,在這裡,我簡單的解釋一下,先來看一下 ViewTypeLookup 的類圖: ViewTypeLookup 的作用就是將localType和globalType相互轉換。那麼怎麼理解這兩個type呢?我們可以這樣認為: localType 是每個子Adapter返回產生的, globalType 是ConcatAdapter產生的。當ConcatAdapter需要將ViewType傳遞給子Adapter,就先要將它的 globalType 轉換成為子Adapter能識別的 localType ;同時,ConcatAdapter產生的ViewType並不是它自己產生的,而是呼叫每個子Adapter的getItemViewType方法獲取,然後然後通過localToGlobal方法轉換成為 globalType 。這一點,我們可以從 ConcatAdapterController 裡面找到答案:

    public int getItemViewType(int globalPosition) {
       // 1. 通過position獲取一個WrapperAndLocalPosition物件,這裡面封裝的是
       // NestedAdapterWrapper和localPosition。
        WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
       // 2. 呼叫NestedAdapterWrapper的getItemViewType方法返回一個ItemViewType。
       // 這裡返回的ViewType就是globalType,NestedAdapterWrapper的內部進行了一次localToGlobal轉換。
        int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition);
        releaseWrapperAndLocalPosition(wrapperAndPos);
        return itemViewType;
    }

如上方法分為兩步,我簡單的總結一下:

  1. globalPosition
    WrapperAndLocalPosition
    NestedAdapterWrapper
    findWrapperAndLocalPosition
    ConcatAdapterController
    globalPosition
    NestedAdapterWrapper
    
  2. NestedAdapterWrapper
    getItemViewType
    NestedAdapterWrapper
    getItemViewType
    getItemViewType
    localType
    ViewTypeLookup
    localToGlobal
    localType
    globalType
    

從整體來說, ViewTypeStorage 是服務於ConcatAdapter,因此不管子Adapter有多少個,只會有一個 ViewTypeStorage 物件;而 ViewTypeLookup 是服務於子Adapter,因此有多少個子Adapter,就會建立多少個 ViewTypeLookup 物件。而 ViewTypeLookup 的建立是在 NestedAdapterWrapper 的構造方法裡面進行的:

    NestedAdapterWrapper(
            Adapter<ViewHolder> adapter,
            final Callback callback,
            ViewTypeStorage viewTypeStorage,
            StableIdStorage.StableIdLookup stableIdLookup) {
        // ······
        mViewTypeLookup = viewTypeStorage.createViewTypeWrapper(this);
       // ······
    }

1.2 隔離ViewType

接下來,我將重點分析ViewType的兩種策略。首先,我們來看隔離策略。從 ConcatAdapterController 的構造方法裡面,我們可以知道,隔離策略用到的 ViewTypeStorage 的實現類是 IsolatedViewTypeStorage 。我們來看一下 IsolatedViewTypeStorage 的實現:

    class IsolatedViewTypeStorage implements ViewTypeStorage {
        SparseArray<NestedAdapterWrapper> mGlobalTypeToWrapper = new SparseArray<>();
        int mNextViewType = 0;
        int obtainViewType(NestedAdapterWrapper wrapper) {
            int nextId = mNextViewType++;
            mGlobalTypeToWrapper.put(nextId, wrapper);
            return nextId;
        }
        @NonNull
        @Override
        public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) {
            NestedAdapterWrapper wrapper = mGlobalTypeToWrapper.get(
                    globalViewType);
            if (wrapper == null) {
                throw new IllegalArgumentException("Cannot find the wrapper for global"
                        + " view type " + globalViewType);
            }
            return wrapper;
        }
        @Override
        @NonNull
        public ViewTypeLookup createViewTypeWrapper(
                @NonNull NestedAdapterWrapper wrapper) {
            return new WrapperViewTypeLookup(wrapper);
        }
        void removeWrapper(@NonNull NestedAdapterWrapper wrapper) {
            for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) {
                NestedAdapterWrapper existingWrapper = mGlobalTypeToWrapper.valueAt(i);
                if (existingWrapper == wrapper) {
                    mGlobalTypeToWrapper.removeAt(i);
                }
            }
        }
        class WrapperViewTypeLookup implements ViewTypeLookup {
            private SparseIntArray mLocalToGlobalMapping = new SparseIntArray(1);
            private SparseIntArray mGlobalToLocalMapping = new SparseIntArray(1);
            final NestedAdapterWrapper mWrapper;
            WrapperViewTypeLookup(NestedAdapterWrapper wrapper) {
                mWrapper = wrapper;
            }
            @Override
            public int localToGlobal(int localType) {
                int index = mLocalToGlobalMapping.indexOfKey(localType);
                if (index > -1) {
                    return mLocalToGlobalMapping.valueAt(index);
                }
                // get a new key.
                int globalType = obtainViewType(mWrapper);
                mLocalToGlobalMapping.put(localType, globalType);
                mGlobalToLocalMapping.put(globalType, localType);
                return globalType;
            }
            @Override
            public int globalToLocal(int globalType) {
                int index = mGlobalToLocalMapping.indexOfKey(globalType);
                if (index < 0) {
                    throw new IllegalStateException("requested global type " + globalType + " does"
                            + " not belong to the adapter:" + mWrapper.adapter);
                }
                return mGlobalToLocalMapping.valueAt(index);
            }
            @Override
            public void dispose() {
                removeWrapper(mWrapper);
            }
        }
    }

針對於 IsolatedViewTypeStorage , 我們重點分析 getWrapperForGlobalType 方法和 createViewTypeWrapper 方法。

  1. getWrapperForGlobalType方法:我們可以從上面的實現可以看出來, NestedAdapterWrapper 物件是從一個數組裡面獲取,其中key是 globalViewType 。那麼 NestedAdapterWrapper 物件是怎麼放進去的呢?我們簡單尋找一下呼叫關係就知道:是在 IsolatedViewTypeStorageobtainViewType 方法放進去的,整個呼叫關係如下圖: 總而言之,就是在 getItemViewType 放入進去的。這裡,我們需要特別的注意,如果ViewType採用隔離策略,那麼子Adapter千萬不能返回相同的ViewType。因為我們從實現來看, NestedAdapterWrapper 是依靠ViewType作為儲存的,那麼如果有兩個Adapter返回相同的ViewType,會導致獲取 NestedAdapterWrapper 不是正確的,也就是前面說的,onCreateViewHolder呼叫的Adapter和onBindViewHolder的Adapter可能不是同一個物件。這個問題在隔離策略應該嚴格避免,否則容易出現莫名其妙的錯誤。

  2. createViewTypeWrapper方法:此方法的作用是用來建立 ViewTypeLookup ,從上面的程式碼中我們可以得知,與 IsolatedViewTypeStorage 對應的 ViewTypeLookup 實現類是 WrapperViewTypeLookup 。從前面的介紹,我們可以知道, createViewTypeWrapper 方法是在 NestedAdapterWrapper 的構造方法裡面被呼叫的,在建立的同時還把 NestedAdapterWrapper 物件傳進來的,這裡就為了後來 localToGlobal 方法裡面儲存 NestedAdapterWrapper 物件埋下了伏筆。前文已經介紹過, ViewTypeLookup 是面向子Adapter的,所以 ViewTypeLookup 記錄的 NestedAdapterWrapper 物件就是跟它對應的 NestedAdapterWrapper 物件。

1.3 共享ViewType

說完了隔離策略的實現,我們再來看看共享策略。從結構來說,共享策略使用的是 SharedIdRangeViewTypeStorage ,同時與它對應的 ViewTypeLookup 實現類是 WrapperViewTypeLookup ;從實現上來說,共享策略在 getWrapperForGlobalType 方法也是通過ViewType獲取 NestedAdapterWrapper 物件,也是在 localToGlobal 方面裡面將記錄的 NestedAdapterWrapper 物件儲存在一個數組裡面,這些跟隔離策略都是一致的。唯一不一致的是, NestedAdapterWrapper 陣列採用的是 SparseArray<List<NestedAdapterWrapper>> 資料資料結構,也就是說,同一個ViewType可能有多個 NestedAdapterWrapper 對應,這也是共享策略的特色,子Adapter可以返回相同的ViewType。那麼相同的ViewType, SharedIdRangeViewTypeStorage 是怎麼確定該返回哪一個 NestedAdapterWrapper 的呢?我們來簡單的看一下 getWrapperForGlobalType 方法的實現:

        public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) {
            List<NestedAdapterWrapper> nestedAdapterWrappers = mGlobalTypeToWrapper.get(
                    globalViewType);
            if (nestedAdapterWrappers == null || nestedAdapterWrappers.isEmpty()) {
                throw new IllegalArgumentException("Cannot find the wrapper for global view"
                        + " type " + globalViewType);
            }
            // just return the first one since they are shared
            return nestedAdapterWrappers.get(0);
        }

看上面的實現,我們可以知道, getWrapperForGlobalType 方法直接返回的是陣列第一個元素。所以,共享策略不能保證,onBindViewHolder使用的ViewHolder是自己Adapter的onCreateViewHolder方法建立來的,這一點大家一定要注意。在這裡,我有一個疑問,既然始終返回的是陣列第一個元素,有必要用一個數組來儲存嗎?我不清楚Google爸爸是怎麼考慮的。

2. 核心處理邏輯:StableId的處理策略

Config裡面還有一個配置就是StableId的模式,從官方的文件來看,我們可以知道stableId一共有三個模式,分別如下:

模式 含義
NO_STABLE_IDS 這個模式比較簡單,就是指Adapter不支援stableId。
ISOLATED_STABLE_IDS 表示子Adapter之間採用隔離策略,在這個模式下,子Adapter不同考慮其他
Adapter的存在,因為在這個模式裡面,ConcatAdapter 會覆蓋子Adapter自
己生成的stableId,由它統一給每個item分配stableId,這樣我們定義子Adapter
的時候,就不用其他的Adapter。注意的是,此時子Adapter的getItemId
方法和ViewHolder的getItemId方法的返回值是不一樣的,我們如果需要stableId
的話,ViewHolder的getItemId方法是最可靠的。
SHARED_STABLE_IDS 表示子Adapter之間採用共享策略,在這個模式,由子Adapter自己生成stableId,
ConcatAdapter不會覆蓋子Adapter的stableId。因為stableId的唯一性原則,所
以每個子Adapter在生成stableId時需要考慮其他子Adapter的存在,必須保證生
成的stableId的唯一性。

stableId的設計跟ViewType的設計非常的類似,都是一個 Storage 類和多個 Lookup 類。在stableId 結構中, StableIdStorage 是服務於ConcatAdapter,因為只會建立一個物件; StableIdLookup 服務於子Adapter,因此每個子Adapter都會建立 StableIdLookup 物件。 我們來簡單的看一下這兩個介面的定義,uml類圖如下: 兩個介面的結構從類圖可以看出,我針對於他們的方法特別解釋一下:

  1. createStableIdLookup方法:顧名思義,就是建立一個 StableIdLookup 物件。在ConcatAdapterController的構造方法中,首先會根據Config裡面配置建立不同的StableIdStorage實現類物件;其次在建立 NestedAdapterWrapper 的時候,會直接呼叫 createStableIdLookup 方法建立一個 StableIdLookup 物件,與新新增進來的子Adapter繫結,子Adapter需要的 StableIdLookup 物件就是在建立的。

  2. localToGlobal方法:將子Adapter轉換成為ConcatAdapter需要的globalId。因為這個方法實現不同,所以就區分出來了三種策略模式。

我們大致瞭解了每個模式的含義,我們分別來看一下每個模式的實現。

2.1 隔離策略

在隔離策略中, StableIdStorage 的實現類是 IsolatedStableIdStorageStableIdLookup 的實現類是 IsolatedStableIdStorage 。 在隔離策略中, IsolatedStableIdStorage 會把將每個子Adapter抹平,因此每個子Adapter生成的stableId都會經過 localToGlobal 方法轉換一次,因此我們直接來看 localToGlobal 方法:

            @Override
            public long localToGlobal(long localId) {
                Long globalId = mLocalToGlobalLookup.get(localId);
                if (globalId == null) {
                    globalId = obtainId();
                    mLocalToGlobalLookup.put(localId, globalId);
                }
                return globalId;
            }

這個方法主要經過兩步:

  1. 判斷快取中是否已經有stableId,如果有,直接返回;如果沒有則進行第二步。

  2. 呼叫 obtainId 獲取一個新的stableId。

從這裡就可以應證前面所說的,隔離策略會覆蓋子Adapter生成的stableId。在隔離策略中,不同的Adapter返回相同的stableId也是沒有關係的,因為不同的Adapter擁有不同的 StableIdLookup 物件,進而 mLocalToGlobalLookup 快取也是不一樣的,所以他們互不影響。

2.2 共享策略

在隔離策略中, StableIdStorage 的實現類是 SharedPoolStableIdStorageStableIdLookup 的實現類是 SameIdLookup 。我們直接來看一下 localToGlobal 方法的實現:

            @Override
            public long localToGlobal(long localId) {
                return localId;
            }

共享策略的實現很簡單,就是將localId作為globalId。從這裡,我們就可以知道為啥使用共享策略時,必須保證子Adapter不能生成不同的stableId。

總結

至此,關於RecyclerView新元件:ConcatAdapter講解完畢。總結如下:

  1. ConcatAdapter的架構主要分為三層,分別是ConcatAdapter、ConcatAdapterController和Helper。

  2. Helper層主要是包括:ViewTypeStorage--用來處理ViewType的;StableIdStorage--用來處理stableId;NestedAdapterWrapper--裡面封裝了子Adapter、ViewTypeStorage和StableIdStorage等相關類。

  3. ViewTypeStorage
    StableIdStorage
    ConcatAdapter
    ViewTypeLookup
    StableIdLookup
    

   微信改了推送機制,真愛請星標本公號 :point_down: