是時候提升你擼RecycleView的效率了

前言

在項目開發中,總離不開列表,說到列表,就會有無窮無盡的 Adapter 須要你去實現。從而出現了不少優秀的 adapter 庫。 不過不少庫功能都很全面了,有不少個類,不少功能,可是實際上只用其中的一兩個我感受是不少人的現狀。因此本身實現一個使用起來順手的是一個不錯的選擇,特別是用在一些不是很複雜的列表時。java

效果

先看看使用效果,激發一下你爲數很少繼續閱讀的激情:git

//單類型列表,默認 LinearLayoutManager
recycleView.setup<NumberInfo> {
    dataSource(initData())
    adapter {
        addItem(R.layout.layout_item) {
            bindViewHolder { data, _, _ ->
                setText(R.id.number, data?.number.toString())
            }
        }
    }
}

//多類型列表
recycleView.setup<Any> {
    withLayoutManager { LinearLayoutManager(context) }
    dataSource(data)
    adapter {
        addItem(R.layout.item_setion_header) {
            isForViewType { data, _ -> data is SectionHeader }
            bindViewHolder { data, _, _ ->
                val header = data as SectionHeader
                setText(R.id.section_title, header.title)
            }
        }
        addItem(R.layout.item_user) {
            isForViewType { data, _ -> data is User }
            bindViewHolder { data, _, _ ->
                val user = data as User
                setText(R.id.name, user.name)
                setImageResource(R.id.avatar, user.avatarRes)
                //若是你的控件找不到方便賦值的方法,能夠經過 findViewById 去查找
                val phone = findViewById<TextView>(R.id.phone)
                phone.text = user.phone
            }
        }
    }
}
複製代碼

嗯....,感受還能夠,最少的狀況下能夠把一個列表代碼用 10 幾行就完成了。github

完整代碼地址

先貼完整代碼地址,沒地址的文章是沒靈魂的:EfficientAdapter
我把它命名爲 EfficientAdapter ,意爲高效的意思,事實上它只有 3 個文件。
至於如何使用,在地址上已經描述了,因此這篇文章主要是講一下實現的思路。app

實現思路

對 Adapter 的封裝,其實無非就是對 Adapter 裏面的幾個回調方法進行封裝罷了,最經常使用的方法是先定義好一個存放 ViewHolder 的列表,而後在各個回調中獲取這些 ViewHolder,而後實現邏輯。框架

那麼其中最操蛋的是哪一個回調方法的封裝呢?我認爲是 getItemViewType。事實上你能夠在不少框架中看到讓你實現獲取 ViewType 的回調方法。ide

一步一步來,先說 ViewHolder 的封裝函數

在 EfficientAdapter 裏面,我把 ViewHolder 的封裝寫成了 BaseViewHolder:ui

class BaseViewHolder(parent: ViewGroup, resource: Int) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(resource, parent, false)
)
複製代碼

這就是個人封裝,夠簡單吧。this

想什麼呢,固然沒這麼簡單,想要在上面使用效果代碼中那樣實現 ViewHolder 的具體邏輯,還須要有 isForViewType,bindViewHolder 等方法。因此我要定義一個類,去提供這些方法:spa

abstract class ViewHolderCreator<T> {
    abstract fun isForViewType(data: T?, position: Int): Boolean
    abstract fun getResourceId(): Int
    abstract fun onBindViewHolder( data: T?, items: MutableList<T>?, position: Int, holder: ViewHolderCreator<T> )

    var itemView: View? = null

    fun registerItemView(itemView: View?) {
        this.itemView = itemView
    }

    fun <V : View> findViewById(viewId: Int): V {
        checkItemView()
        return itemView!!.findViewById(viewId)
    }

    private fun checkItemView() {
        if (itemView == null) {
            throw NullPointerException("itemView is null")
        }
    }
}
複製代碼

在 ViewHolderCreator 中,getResourceId 和 onBindViewHolder 方法相信都知道是幹嗎的,而 isForViewType 方法是用來判斷 ViewType 的,注意它返回類型是 Boolean,這個方法會在下面講到。由於我想在 onBindViewHolder 中能方便的拿到 view,因此有了 registerItemView 和 findViewById 等其餘方法。

以上就是 ViewHolder 的全部封裝,接下來就對 Adapter 的封裝。

open class EfficientAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    var items: MutableList<T>? = mutableListOf()
    private val typeHolders: SparseArrayCompat<ViewHolderCreator<T>> = SparseArrayCompat()
}
複製代碼

Adapter 首先須要一個泛型用來表示傳入的實體類類型,定義了一個 item 列表用來作數據源。ViewHolder 的集合使用一個 SparseArrayCompat 去存儲。之因此用 SparseArray ,是由於我想把 ViewType 作爲 key。

因此,在 onCreateViewHolder 回調方法中,須要根據 viewType 參數在 typeHolders 中取到具體的 ViewHolderCreator:

private fun getHolderForViewType(viewType: Int): ViewHolderCreator<T>? {
    return typeHolders.get(viewType)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val holder = getHolderForViewType(viewType)
            ?: throw NullPointerException("No Holder added for ViewType $viewType")
    return BaseViewHolder(parent, holder.getResourceId())
}
複製代碼

這樣,就能夠經過 getHolderForViewType 方法,在 typeHolders 中獲取到對應的 ViewHolderCreator,而後根據 ViewHolderCreator 中的信息去建立一個新的 ViewHolder。若是找不到,就拋一個空指針異常。

一樣道理,onBindViewHolder 回調方法也能夠這麼作:

override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
    onBindViewHolder(viewHolder, position, mutableListOf())
}

override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int, payloads:MutableList<Any>) {
    val holder = getHolderForViewType(viewHolder.itemViewType)
            ?: throw NullPointerException("No Holder added for ViewType " + viewHolder.itemViewType)
    holder.registerItemView(viewHolder.itemView)
    holder.onBindViewHolder(items?.get(position), items, position, holder)
}
複製代碼

注意的是 onBindViewHolder 回調方法有兩個,他們的區別就不說了,這裏兩個都實現了邏輯,固然你也能夠只實現一個。

還剩下 getItemCount 和 getItemViewType 回調方法了,getItemCount 其實沒什麼好說的:

override fun getItemCount(): Int = items?.size ?: 0
複製代碼

先不說如何實現 getItemViewType ,先說說怎麼添加數據到 typeHolders 中:

fun register(holder: ViewHolderCreator<T>) = apply {
    var viewType: Int = typeHolders.size()
    while (typeHolders.get(viewType) != null) {
        viewType++
    }
    typeHolders.put(viewType, holder)
}
複製代碼

typeHolders 的類型是 SparseArrayCompat,這裏我用 ViewType 做爲 key,register 方法中,能夠看到沒每註冊一次,viewType 就自動加一(由於 typeHolders 的長度會變長),達到了不會重複的效果,到時候在實現 getItemViewType 的時候,就直接取出來便可。避免了具體業務的干擾。

最後看看 getItemViewType 的實現:

override fun getItemViewType(position: Int): Int {
    if (items == null) {
        throw NullPointerException("adapter data source is null")
    }
    for (i in 0 until typeHolders.size()) {
        val holder = typeHolders.valueAt(i)
        val data = items?.getOrNull(position)
        if (holder.isForViewType(data, position)) {
            return typeHolders.keyAt(i)
        }
    }

    //找不到匹配的 viewType
    throw NullPointerException(
            "No holder added that matches at position=$position in data source")
}
複製代碼

該方法的思路是經過遍歷 typeHolders,經過 ViewHolderCreator 的 isForViewType 方法來判斷是否符合條件,若是符合,則在 typeHolders 中取出 viewType 出來返回。

由於 typeHolders 中的 viewType 是自增的,因此 getItemViewType 的返回值會是 0,1,2,3...

isForViewType 在實際中如何實現?

舉個例子:
若是你的數據源由多個實體類組成,好比:

private List<Object> data = new ArrayList<>();
data.add(new User("Marry", 17, R.drawable.icon2, "123456789XX"));
data.add(new SectionHeader("My Images"));
data.add(new Image(R.drawable.cover1));
複製代碼

那麼在構建 EfficientAdapter 時,泛型傳入的天然是 Object,而後在 isForViewType 方法中你能夠這樣區分類型:

// 表明這是 User 類型
 public boolean isForViewType(Object data, int position) {
   return data instanceof User;
}

 // 表明這是 SectionHeader 類型
 public boolean isForViewType(Object data, int position) {
   return data instanceof SectionHeader;
}

 // 表明這是 Image 類型
 public boolean isForViewType(Object data, int position) {
   return data instanceof Image;
}
複製代碼

若是你的數據源只有一個實體類,可是實體類裏面有某個字段能夠區分類型,你能夠這樣:

// 表明這是 User 類型
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.USER
}

 // 表明這是 SectionHeader 類型
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.HEADER
}

 // 表明這是 Image 類型
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.IMAGE
}
複製代碼

其餘狀況能夠根據具體的狀況而定。

到這裏,已經完成 Adapter 的封裝了,接下來能夠定義一些數據源的增刪查改的方法,好比:

//綁定 RecyclerView
fun attach(recyclerView: RecyclerView) = apply { recyclerView.adapter = this }

//提交數據
fun submitList(list: MutableList<T>) {
    this.items?.clear()
    this.items?.addAll(list)
    notifyDataSetChanged()
}
複製代碼

到這裏,已經能夠簡單粗暴的使用了:

adapter = EfficientAdapter<SectionHeader>()
        .register(object : ViewHolderCreator<SectionHeader>() {
            override fun isForViewType(data: SectionHeader?, position: Int) = data != null
            override fun getResourceId() = R.layout.item_setion_header

            override fun onBindViewHolder( data: SectionHeader?, items: MutableList<SectionHeader>?, position: Int, holder: ViewHolderCreator<SectionHeader> ) {
                setText(R.id.section_title, data.title)
            }
        }).attach(recycle_view)
adapter?.submitList(data)
複製代碼

但和使用效果差異有點大啊。因此,接下來就是 kotlin 發揮的時候了。

擴展函數 與 DSL

相信學過 kotlin 的都知道這兩個東西,他們能夠爲咱們的代碼提供更多的可能。

ViewHolderCreator DSL

因爲 ViewHolderCreator 是一個抽象類,對它進行 DSL 封裝須要一個默認的實現類(也許能夠直接封裝,可是我只能想到這種方法):

class ViewHolderDsl<T>(private val resourceId: Int) : ViewHolderCreator<T>() {
    private var viewType: ((data: T?, position: Int) -> Boolean)? = null
    private var viewHolder: ((data: T?, position: Int, holder: ViewHolderCreator<T>) -> Unit)? = null

    fun isForViewType(viewType: (data: T?, position: Int) -> Boolean) {
        this.viewType = viewType
    }

    fun bindViewHolder(holder: (data: T?, position: Int, holder: ViewHolderCreator<T>) -> Unit) {
        viewHolder = holder
    }

    override fun isForViewType(data: T?, position: Int): Boolean {
        return viewType?.invoke(data) ?: (data != null)
    }

    override fun getResourceId() = resourceId

    override fun onBindViewHolder( data: T?, items: MutableList<T>?, position: Int, holder: ViewHolderCreator<T>, payloads: MutableList<Any> ) {
        viewHolder?.invoke(data, position, holder)
    }
}
複製代碼

代碼比較清晰,就是對三個抽象方法的實現。因爲 getResourceId 比較簡單,因此直接放在構造方法中傳值就好。

實現好了 ViewHolderDsl,咱們給 EfficientAdapter 定義一個擴展函數,用 DSL 的方式去調用 register 方法:

fun <T : Any> EfficientAdapter<T>.addItem(resourceId: Int, init: ViewHolderDsl<T>.() -> Unit) {
    val holder = ViewHolderDsl<T>(resourceId)
    holder.init()
    register(holder)
}
複製代碼

比較簡單,就是建立好 ViewHolderDsl 後,調用 register 方法便可。

到這裏,其實已經能夠用了,只要咱們再寫一個函數,用 DSL 的方式建立 Adapter 便可:

fun <T : Any> efficientAdapter(init: EfficientAdapter<T>.() -> Unit): EfficientAdapter<T> {
    val adapter = EfficientAdapter<T>()
    adapter.init()
    return adapter
}
複製代碼

因此上面那個簡單粗暴的示例代碼就能夠變成這樣:

adapter = efficientAdapter<Any> {
    addItem(R.layout.item_setion_header) {
        isForViewType { it != null }
        bindViewHolder { data, _, _ ->
            setText(R.id.section_title, data.title)
        }
    }
}.attach(recycle_view)
adapter?.submitList(data)
複製代碼

代碼又清晰和簡單了不少。因爲在 ViewHolderDsl 中,isForViewType 的默認實現是 data!=null,因此若是是單類型列表,這個方法能夠直接不寫。

雖然代碼簡單了不少,但這樣總要定義 adapter 對象和綁定 RecycleView,因此更加優雅的方式就是給 RecycleView 定義一個擴展函數,把這些操做都包裝起來。

首先咱們實現一個叫 RecycleSetup 的類,在這個類裏面,把 RecycleView 的配置以及 Adapter 操做,數據源操做等統統包裝起來:

class RecycleSetup<T> internal constructor(private val recyclerView: RecyclerView) {

    var items = mutableListOf<T>()
    var adapter: EfficientAdapter<T>? = null
    var context = recyclerView.context

    fun dataSource(items: MutableList<T>) {
        this.items.clear()
        this.items = items
    }

    fun withLayoutManager(init: RecycleSetup<T>.() -> RecyclerView.LayoutManager) =
            apply { recyclerView.layoutManager = init() }

    fun adapter(init: EfficientAdapter<T>.() -> Unit) {
        this.adapter = EfficientAdapter()
        init.invoke(adapter!!)
        recyclerView.adapter = adapter
        adapter?.submitList(this.items)
    }

    fun submitList(list: MutableList<T>) {
        this.items.clear()
        this.items = list
        adapter?.submitList(this.items)
    }

    fun getItem(position: Int): T = items[position]
}
複製代碼

代碼簡單,相信你們都能看懂。

有了這個類,最後,就能夠給 RecycleView 實現擴展函數了:

fun <T> RecyclerView.setup(block: RecycleSetup<T>.() -> Unit): RecycleSetup<T> {
    val setup = RecycleSetup<T>(this).apply(block)
    if (layoutManager == null) {
        layoutManager = LinearLayoutManager(context)
    }
    return setup
}

fun <T> RecyclerView.submitList(items: MutableList<T>) {
    if (adapter != null && adapter is EfficientAdapter<*>) {
        (adapter as EfficientAdapter<T>).submitList(items)
    }
}
複製代碼

layoutManager 爲空就默認實現 LinearLayoutManager。最後,上面那個簡單粗暴的代碼就能夠寫成跟一開始說那個效果同樣了:

recycleView.setup<SectionHeader> {
    adapter {
        addItem(R.layout.item_setion_header) {
            bindViewHolder { data, _, _ ->
                setText(R.id.section_title, data.title)
            }
        }
    }
}
recycleView.submitList(data)
複製代碼

完整代碼和例子都在這裏 EfficientAdapter ,有興趣能夠看看。

總結

其實,整篇文章的代碼思路都比較簡單,其中比較有意思的是 viewType 自加一這裏,在使用的時候用戶只須要實現 isForViewType 便可,固然相比各個大佬們的庫,這個算是比較簡單的,大佬們閉上眼睛都能寫出來,因此寫這篇文章的緣由是分享本身在封裝代碼的時候的一個思路,一步一步從零到有。相信不少人都須要這種東西,比成天搬磚有意思,也會學到一點點知識。

Thank you for 看完整篇文章,完!

相關文章
相關標籤/搜索