作者: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的風格是什麼樣子,具有很大的靈活性和擴充性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
itemManager.refreshAll { val books = viewModel.getBooks() val bookShelfs = viewModel.getBookShelfs() header { text = "DSL header" color = Color.BLUE } book.foreach { book -> bookItem { title = if (book.id != 0) book.title else "Empty Book" date = book.returnDate url = book.imageUrl } } bookShelfs.foreachIndexed { index, bookShelf -> bookShelf { title = "Number$index Shelf - ${bookShelf.name}" size = bookShelf.size url = bookShelf.imageUrl onclick { startActivity<BookShelfActivity>("id" to bookShelf.id) } } } footer { text = "Load More" onClick { loadMore() } } } |
核心類概覽
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。
然後怎麼寫呢?
- 先定義一個Item,我們就叫它
SingleTextItem.kt
這個Item裡面需要包含一個字串,將來在OnBindVH
的代理中傳入到View
中
1234567891011/*** 你自己定義的Item 示例:只有一個Text的Item* your custom Item* example: a RecyclerView Item contain a single TextView*/class SingleTextItem(val content: String) : Item {override val controller: ItemControllerget() = TODO("Controller Need")} - 然後我們需要些這類Item對於的邏輯,也就是
ItemController
,在伴生物件中進行實現
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758/*** 你自己定義的Item 示例:只有一個Text的Item* your custom Item* example: a RecyclerView Item contain a single TextView*/class SingleTextItem(val content: String) : Item {/*** implements these functions to delegate the core method of RecyclerView's Item*/companion object Controller : ItemController {override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {val inflater = parent.context.layoutInflaterval view = inflater.inflate(R.layout.item_single_text, parent, false)val textView = view.findViewById<TextView>(R.id.tv_single_text)return ViewHolder(view, textView)}override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) {/*** with the help of Kotlin Smart Cast, we can cast the ViewHolder and item first.* the RecyclerView DSL framework could guarantee the holder and item are correct, just cast it !** 因為Kotlin的智慧Cast 所以後面我們就不需要自己強轉了* DSL 框架可以保證holder和item的對應性*/holder as ViewHolderitem as SingleTextItem/*** what you do in OnBindViewHolder in RecyclerView, just do it here*/holder.textView.text = item.content}/*** define your ViewHolder here to pass view from OnCreateViewViewHolder to OnBindViewHolder* this ViewHolder class should be private and only use in this scope** 在這裡宣告此Item所對應的ViewHolder,用來從OnCreateViewHolder傳View到OnBindViewHolder中。* 這個ViewHolder類應該是私有的,只在這裡用*/private class ViewHolder(itemView: View?,val textView: TextView) : RecyclerView.ViewHolder(itemView)}/*** ItemController is necessary , it is often placed in the Item's companion Object* DON'T new an ItemController , because item viewType is corresponding to ItemController::class.java* or you will get many different viewType (for one type really) , which could break the RecyclerView's Cache** 一般來講,我們把ItemController放在Item的伴生物件裡面,不要在這裡new ItemController,因為在自動生成ViewType的時候,* 我們是根據ItemController::class.java 來建立一一對應關係,如果是new的話,會導致無法相等以至於生成許多ItemType,這樣子會嚴重破壞Recyclerview的快取機制*/override val controller: ItemControllerget() = Controller} - 寫個擴充函式,來讓它支援DSL
1234567/*** wrap the add SingleTextItem function with DSL style** 用DSL來風格來簡單保證add SingleTextItem的操作*/fun MutableList<Item>.singleText(content: String) = add(SingleTextItem(content)) - 來試試把,用一下~
123456789val recyclerView: RecyclerView = findViewById(R.id.recyclerview)recyclerView.layoutManager = LinearLayoutManager(this)recyclerView.withItems {repeat(10) {singleText("this is a single Text: $it")}}
複雜情景討論
情景1: 同一個Item下,對於ViewStyle的不同處理
方案:Item中除了必要的資料類,再傳入一個 YourView.() -> Unit
型別的可空?
閉包。
原理蠻簡單,就弄程式碼了,註釋很全… 還是中英雙語的呢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
package cn.edu.twt.retrox.recyclerviewdsldemo import android.support.v7.widget.RecyclerView import android.view.View import android.view.ViewGroup import android.widget.TextView import cn.edu.twt.retrox.recyclerviewdsl.Item import cn.edu.twt.retrox.recyclerviewdsl.ItemController import org.jetbrains.anko.layoutInflater /** * Just do something new with DSL * we could pass View.() -> Unit */ class SingleTextItemV2(val content: String, val init: TextView.() -> Unit) : Item { /** * implements these functions to delegate the core method of RecyclerView's Item */ companion object Controller : ItemController { override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { val inflater = parent.context.layoutInflater val view = inflater.inflate(R.layout.item_single_text, parent, false) val textView = view.findViewById<TextView>(R.id.tv_single_text) return ViewHolder(view, textView) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) { /** * with the help of Kotlin Smart Cast, we can cast the ViewHolder and item first. * the RecyclerView DSL framework could guarantee the holder and item are correct, just cast it ! * * 因為Kotlin的智慧Cast 所以後面我們就不需要自己強轉了 * DSL 框架可以保證holder和item的對應性 */ holder as ViewHolder item as SingleTextItemV2 /** * what you do in OnBindViewHolder in RecyclerView, just do it here */ holder.textView.text = item.content // custom settings for TextView passed by DSL holder.textView.apply(item.init) } /** * define your ViewHolder here to pass view from OnCreateViewViewHolder to OnBindViewHolder * this ViewHolder class should be private and only use in this scope * * 在這裡宣告此Item所對應的ViewHolder,用來從OnCreateViewHolder傳View到OnBindViewHolder中。 * 這個ViewHolder類應該是私有的,只在這裡用 */ private class ViewHolder(itemView: View?, val textView: TextView) : RecyclerView.ViewHolder(itemView) } /** * ItemController is necessary , it is often placed in the Item's companion Object * DON'T new an ItemController , because item viewType is corresponding to ItemController::class.java * or you will get many different viewType (for one type really) , which could break the RecyclerView's Cache * * 一般來講,我們把ItemController放在Item的伴生物件裡面,不要在這裡new ItemController,因為在自動生成ViewType的時候, * 我們是根據ItemController::class.java 來建立一一對應關係,如果是new的話,會導致無法相等以至於生成許多ItemType,這樣子會嚴重破壞Recyclerview的快取機制 */ override val controller: ItemController get() = Controller override fun areContentsTheSame(newItem: Item): Boolean { return newItem is SingleTextItemV2 && content == newItem.content } override fun areItemsTheSame(newItem: Item): Boolean = this.areContentsTheSame(newItem) } /** * wrap the add SingleTextItem function with DSL style * * 用DSL來風格來簡單保證add SingleTextItem的操作 */ fun MutableList<Item>.advancedText(content: String, init: TextView.() -> Unit) = add(SingleTextItemV2(content, init)) |
情景2 : 可重新整理列表
比如說,分頁載入,列表變化,和其他所有可變的Recyclerview列表
方案:這種情況下,我們把ItemManager
拿出來單獨操作即可,善用autorefresh
方法和DiffUtil
1 2 3 4 5 6 7 8 9 10 |
lateinit var itemManager: ItemManager val recyclerView: RecyclerView = findViewById(R.id.recyclerview) recyclerView.layoutManager = LinearLayoutManager(this) itemManager = ItemManager() recyclerView.adapter = ItemAdapter(itemManager) itemManager.autoRefresh { // do something here // see cn.edu.twt.retrox.recyclerviewdsldemo.act.DiffRefreshListAct } |
想要更加好的重新整理體驗,就要先給給RecyclerviewDSL加入DiffUtil的能力 :
1 2 3 4 5 6 7 8 |
interface Item { val controller: ItemController fun areItemsTheSame(newItem: Item): Boolean = false fun areContentsTheSame(newItem: Item): Boolean = false } |
實現Item介面的時候, 重寫後面那倆預設方法即可。
比如說我們要做一個列表,列表裡面是一堆文字的item,在最末尾有一個Button,點選Button就會讓文字Item新增10個。然後在autoRefresh
的閉包中,我們只需要用DSL來表達這個需求即可。框架會幫我們做這一切。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * function autoRefresh don't wipe the data of list * you should customize the thing needed to do when it refresh (it create a snapshot of list internally and use DiffUtil) * in this function : Every Time we refresh , remove the last Button item , then add some Text Item, at Last we add the button at Last */ itemManager.autoRefresh { if (size > 0 && last() is ButtonItem) removeAt(size - 1) // 如果最後一個是ButtonItem 移除 val currentSize = size repeat(10) { advancedText("This is Item : ${currentSize + it}") { textSize = if (it > 5) 14f else 18f } } buttonItem("Add Items") { // 新增ButtonItem setOnClickListener { refreshList() } } } |
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裡面,然後
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class CourseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val cardView: CardView = itemView.findViewById(R.id.cv_item_course) val textView: TextView = itemView.findViewById(R.id.tv_item_course) fun bind(course: Course) { // balabalabla //各種邏輯各種邏輯 } } // 然後你的OnBindViewHolder方法就簡單多了 override fun onBindViewHolder(holder: CourseViewHolder, position: Int) { //這裡可以用ViewType / holder instance of 來處理多種VH val course = courseList[position] holder.bind(course) } |
在這種架構下,可以把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的架構中,確定一類檢視到底需要什麼?哪些東西可以用一個最小的集合來定義一類檢視?
我們來梳理一下:
1 2 3 4 |
展現給使用者看的東西 = 檢視 + 填充資料 檢視 <- OnCreateViewHolder中相關邏輯 資料填充 <- OnBindViewHolder中把資料Set到View中 |
所以說,只要我們把OnCreateVH
,OnBindVH
的邏輯代理出去,就可以把一類Item的檢視部分進行完整的解耦。給太子端程式碼!
1 2 3 4 5 |
interface ItemController { fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder // 檢視 fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) // 這裡還需要具體實現 -> 檢視填充 } |
現在我們解耦出了檢視,還剩下檢視的資料填充。一般來講,Model資料型別和ViewHolder型別一一對應,因此我們可以認為一種ItemController對應著一個型別的Item(一般就是嵌入的一個data Class)
於是我們把資料類嵌入進去
1 2 3 4 |
interface Item { val controller: ItemController // 這裡應該用companion object } |
比如說我們有一個高度定製的TextView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class IndicatorTextItem(val text: String) : Item { private companion object Controller : ItemController { override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { val view = parent.context.layoutInflater.inflate(R.layout.schedule_item_indicator, parent, false) return IndicatorTextViewHolder(view) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) { holder as IndicatorTextViewHolder item as IndicatorTextItem holder.indicatorTextView.text = item.text } private class IndicatorTextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val indicatorTextView: TextView = itemView.findViewById(R.id.tv_course_indicator) } } override val controller: ItemController get() = Controller } |
在這裡,我們就已經把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
1 2 3 4 5 6 |
// controller to view type private val c2vt = mutableMapOf<ItemController, Int>() // view type to controller private val vt2c = mutableMapOf<Int, ItemController>() |
因為要保證Key,Value的相互之前快速索引,因此需要同時管理這兩個Map。
新增Item時自動註冊 + ViewType自動生成 :Item介面要求必須有一個controller
成員變數,因此在新增到Item List的同時,進行監聽。不如來看看程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
object ItemControllerManager { private var viewType = 0 // object保證了單例 因此ViewType肯定是從0開始 // controller to view type private val c2vt = mutableMapOf<ItemController, Int>() // view type to controller private val vt2c = mutableMapOf<Int, ItemController>() /** * 檢查Item(對應的controller)是否已經被註冊,如果沒有,那就註冊一個ViewType */ fun ensureController(item: Item) { val controller = item.controller if (!c2vt.contains(controller)) { c2vt[controller] = viewType vt2c[viewType] = controller viewType++ } } /** * 對於一個Collection的ViewType註冊,先進行一次去重 */ fun ensureControllers(items: Collection<Item>): Unit = items.distinctBy(Item::controller).forEach(::ensureController) /** * 根據ItemController獲取對應的Item -> 代理Adapter.getItemViewType */ fun getViewType(controller: ItemController): Int = c2vt[controller] ?: throw IllegalStateException("ItemController $controller is not ensured") /** * 根據ViewType獲取ItemController -> 代理OnCreateViewHolder相關邏輯 */ fun getController(viewType: Int): ItemController = vt2c[viewType] ?: throw IllegalStateException("ItemController $viewType is unused") } |
在Adapter 的資料來源修改時,呼叫相關的ensureControllers
方法來完成相關的註冊。同時Adapter中,相關的邏輯也可以被這裡的ItemController代理,程式碼差不多是這樣子的:
1 2 3 4 5 6 7 |
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemManager.getController(viewType).onCreateViewHolder(parent) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = itemManager[position].controller.onBindViewHolder(holder, itemManager[position]) |
在這種情況下,Adapter的兩個核心方法就被代理出去了,實現了不同VH邏輯的隔離。
關於自動註冊ItemType,我們的做法是實現MutableList介面,內部組合一個普通的MutableList,對add
,addAll
,remove
之類方法進行AOP處理,這些方法的執行的同時,自動檢測或者註冊ItemController
,同時對於Adapter進行相應的Notify,這樣子就可以實現一個輕量級的MVVM。
在這裡,其實我們可以做很多事情,比如說代理出DiffUtil來進行自動Diff
1 2 3 4 5 6 7 8 |
interface Item { val controller: ItemController fun areItemsTheSame(newItem: Item): Boolean = false fun areContentsTheSame(newItem: Item): Boolean = false } |