構建Recyclerview DSL

kotliner發表於2018-06-02

作者:RetroX
原文連結:https://github.com/life2015/RecyclerViewDSL/blob/master/README_ZH.md

接文章 DSL in action

上一篇文章說了如何把DSL用在專案的佈局中,而這篇文章來講講怎麼把DSL用在Recyclerview中。此框架已經在我的專案中大規模使用,並且極大地提高了Recyclerview列表構建效率和複用能力。

特色

  • 輕量級(只有一個Kotlin檔案)
  • 可擴充(你可以完全自定義自己的Item)
  • 易用(它只是對Rec的OnCreateVH OnBindVH做了代理,不需要額外的學習成本)
  • 寫著爽(Anko風格寫法,DSL配置列表靈活易用)

看看效果?

這是一個大概的效果,Recyclerview DSL中,我們可以用DSL的風格去配置Item被如何加入到Rec,各個Item的風格是什麼樣子,具有很大的靈活性和擴充性。

核心類概覽

  • Item: Recyclerview DSL中,用來儲存View對應資料的類,比如說TextView的字串,Imageview的url等等,基本上可以認為是擔任著ViewModel的角色
  • ItemController: 一般內嵌在Item類的Companion Object中,用於代理Item相關的OnCreateVH,OnBindVH邏輯,基本上一個Item的View邏輯和業務邏輯在這裡表現。
  • ItemAdapter:Recyclerview DSL所依賴的Adapter,在初始化的時候會用到,後面它很少出面了
  • ItemManager: RecyclerView DSL的Adapter的一個核心成員變數,統管著Adapter的Item和相應的ItemController,比如說他們的重新整理,新增,刪除。DSL的語法特性擴充,基本上在這裡表現。

那怎麼用?

  • 定義列表要用的Item(可以全域性複用 所以要好好設計)
  • 寫一個MutableList的擴充
  • 開始使用!

舉個例子?

比如說我要寫定義一類Item,這類Item就是一個FrameLayout裡面包了個TextView。

然後怎麼寫呢?

  1. 先定義一個Item,我們就叫它SingleTextItem.kt
    這個Item裡面需要包含一個字串,將來在OnBindVH的代理中傳入到View
  2. 然後我們需要些這類Item對於的邏輯,也就是ItemController,在伴生物件中進行實現
  3. 寫個擴充函式,來讓它支援DSL
  4. 來試試把,用一下~

複雜情景討論

情景1: 同一個Item下,對於ViewStyle的不同處理

方案:Item中除了必要的資料類,再傳入一個 YourView.() -> Unit型別的可空?閉包。

原理蠻簡單,就弄程式碼了,註釋很全… 還是中英雙語的呢

情景2 : 可重新整理列表

比如說,分頁載入,列表變化,和其他所有可變的Recyclerview列表

方案:這種情況下,我們把ItemManager拿出來單獨操作即可,善用autorefresh方法和DiffUtil

想要更加好的重新整理體驗,就要先給給RecyclerviewDSL加入DiffUtil的能力 :

實現Item介面的時候, 重寫後面那倆預設方法即可。
比如說我們要做一個列表,列表裡面是一堆文字的item,在最末尾有一個Button,點選Button就會讓文字Item新增10個。然後在autoRefresh的閉包中,我們只需要用DSL來表達這個需求即可。框架會幫我們做這一切。

AutoRefresh背後的原理就是,在呼叫閉包前,對Adapter的Item做一個SnapShot,然後對比AutoRefresh閉包使用之後的ItemList情況,最後使用DiffUtil來處理。

如果你是要對列表進行全量重新整理,可以直接使用refreshll方法,此方法會清除列表然後再新增新的Item,當然這個過程是有DiffUtil參與的。

原理/動機分析

常規開發

如果按照普通的開發流程,構建列表的時候,一般就是 Adapter + List。 Adapter裡面包含著ViewHolder的建立和繫結邏輯,這樣子在大規模開發迭代中會遇到的一個問題是:Adapter的邏輯越堆積越重,比如說在OnBindViewHolder方法中包含著重度的業務邏輯,getItemViewType,onCreateViewHolder中包含著大量的樣板程式碼。

  • 定義ViewType常量
  • getItemViewType中各種判斷
  • OnCreateViewHolder中做建立
  • OnBindViewHolder做資料繫結

這些程式碼都會堆積在Adapter中,時間一長,Type一多,Adapter寫起來就會很蛋疼。另外,ViewType/ViewHolder/BindViewHolder邏輯都很難去複用,因為他們是寫死在ViewHolder裡面的。

簡單最佳化一下?

我們開始思考,這些東西是不是可以解耦開呢?

於是你覺得,OnBindViewHolder的邏輯可以寫在ViewHolder裡面,然後

在這種架構下,可以把ViewHolder獨立開,解耦一部分Adapter中的邏輯。嗯… 還可以(沒啥技術含量)

問題/不足

  • ViewHolder複用問題:
    我們只解耦了OnBindViewHolder的邏輯,但OnCreateViewHolder還是要再寫
  • 複用靈活性問題:
    比如說我在複用的時候,Adapter1裡面對CardView要設定1dp的陰影,Adapter2裡面需要3dp。
    Adapter1裡面對這類ViewHolder裡面的TextView要設定:字型,顏色,字號。Adapter2裡面需要另外的配置。
    又比如說,Adapter1裡面對於不同地方的同類ViewHolder裡面的TextView要設定:字型,顏色,字號等等….
  • ViewType問題:
    我們真的需要手動指定ViewType嗎,因為經過我的一番思考,ViewType和ViewHolder::class.java在合理的封裝下,可以是1對1的關係。

再次思考 – 到底要怎麼解耦?

於是我開始思考在Recyclerview的架構中,確定一類檢視到底需要什麼?哪些東西可以用一個最小的集合來定義一類檢視?

我們來梳理一下:

所以說,只要我們把OnCreateVH,OnBindVH的邏輯代理出去,就可以把一類Item的檢視部分進行完整的解耦。給太子端程式碼!

現在我們解耦出了檢視,還剩下檢視的資料填充。一般來講,Model資料型別和ViewHolder型別一一對應,因此我們可以認為一種ItemController對應著一個型別的Item(一般就是嵌入的一個data Class)

於是我們把資料類嵌入進去

比如說我們有一個高度定製的TextView

在這裡,我們就已經把IndicatorTextView這個Recyclerview Item的檢視層和資料填充都解耦了出來。只需要塞進去IndicatorTextItem物件,就可以做到相應的效果。並且這個Item可以在多個Recyclerview Adapter中複用。

Adapter如何協調?

與這套解耦相配合的是一套Adapter的封裝,來對接相關的介面完成對應邏輯的解耦已經ViewType的分配

對於Adapter,我們需要完成的邏輯就是 ItemController ViewType的轉換。

一個理論前提是:在高度封裝的情況下,ViewType並沒有具體的語義,它的作用在於區分不同的ItemController。而對於具體的語義,則轉到Item那邊來表示,比如說上面的class IndicatorTextItem(val text: String) : Item

落實到方法上:我們可以實現一套ItemController ViewType的序號產生器制,那麼這套機制的具體需求是什麼?應該怎樣設計?先列下需求:

  • 一對一的關係 支援相互索引
  • 照顧ViewHolder的全域性複用
  • ViewType自動生成
  • 新增Item時自動註冊

一對一的關係 支援相互索引:我們可以維護兩個Map

因為要保證Key,Value的相互之前快速索引,因此需要同時管理這兩個Map。

新增Item時自動註冊 + ViewType自動生成 :Item介面要求必須有一個controller成員變數,因此在新增到Item List的同時,進行監聽。不如來看看程式碼

在Adapter 的資料來源修改時,呼叫相關的ensureControllers方法來完成相關的註冊。同時Adapter中,相關的邏輯也可以被這裡的ItemController代理,程式碼差不多是這樣子的:

在這種情況下,Adapter的兩個核心方法就被代理出去了,實現了不同VH邏輯的隔離。

關於自動註冊ItemType,我們的做法是實現MutableList介面,內部組合一個普通的MutableList,對add,addAll,remove之類方法進行AOP處理,這些方法的執行的同時,自動檢測或者註冊ItemController,同時對於Adapter進行相應的Notify,這樣子就可以實現一個輕量級的MVVM。

在這裡,其實我們可以做很多事情,比如說代理出DiffUtil來進行自動Diff

相關文章