一百行代碼造一個 RecyclerView.Adapter 輪子

市面上已經有不少開源且好用的 Adapter 庫,爲啥非要本身造輪子呢?
java


學習和工做中我經常使用過 BaseRecyclerViewAdapterHelperSugarAdapter,也都實現過複雜的列表,倒也不能說很差用或者不順手,只是和本身的理念不太匹配,一個簡單的 Adapter 爲啥都要搞得那麼複雜呢,因而決定按照本身的想法造一個簡單高效的輪子。git

Adapter 是幹什麼的

Adapter 適配器,顧名思義,是兩類不一樣東西之間溝通的橋樑,RecyclerView.Adapter 則是爲了實現數據到視圖的適配,說白了就是一種 Model -> View 的映射關係。
本着「單一職責」的原則,這裏造的輪子只考慮 Adapter 的本職工做,不考慮添加什麼「頭部」、「尾部」之類的東西,簡單高效地實現 Model -> View 的映射。github

這裏先放下 Demo 中的截圖,Demo 中會有 6 種卡片樣式, 4 種數據類型和 3 種自定義的 ViewHolder。
緩存

SimpleAdapter 實現過程

這裏主要講述下實現的一些思路,和大體的實現,無論興趣的能夠直接看 Demo 的演示。bash

先實現一個 ViewHolder

RecyclerView.ViewHolder 承載着渲染數據的視圖,可是沒有對數據作任何封裝。這裏最基本的加個數據的泛型,緩存下當前的數據,提供一個 onBind 方法來接收 onBindViewHolder 時的數據。這樣就實現了 SimpleAdapter 的第一個類 SimpleHolder,至於佈局文件和渲染邏輯,以後再說。架構

open class SimpleHolder<T: Any>(v: View) : RecyclerView.ViewHolder(v) {
    /**
     * holder 的當前數據
     */
    var data: T? = null
    @CallSuper
    open fun onBind(data: T) { this.data = data }
    //...
}
複製代碼

如何配置一條映射關係

這裏的「一條映射關係」指的是:我拿到了一個數據 T -> 根據 T 裏的數據確認下要渲染成什麼樣的視圖 -> 這個視圖用什麼佈局文件 -> 數據的渲染邏輯用什麼 ViewHolder。也就是說「一條映射關係」是一種 ViewHolder 對某一類數據的渲染方式。因爲 RecyclerViewViewHolder 緩存是以 getItemViewType 的值爲 key 的進行的,所以「一條映射關係」的配置須要與惟一的 viewType 相匹配。maven

這裏須要定義一個配置項,來配置一條映射關係,在定義以前,先來考慮下映射過程當中都涉及到哪些東西:工具

  • 首先是數據類型:Class<T>
  • 有時候咱們但願一樣類型的數據,在不一樣狀況下,渲染成不一樣的視圖,增長一個過濾的 lambda:(T)->Boolean
  • 視圖方面首當其衝的固然是佈局文件:LayoutRes
  • 渲染邏輯複雜的還但願可以自定義 ViewHolderClass<out SimpleHolder<T>>
  • 因爲 RecyclerView 的緩存機制,這裏也要惟一肯定一個:viewType
  • 咱們常常須要在 ViewHolder 建立時作一些通用邏輯:(SimpleHolder<T>)->Unit
  • 固然還有 onBindViewHolder 時:(SimpleHolder<T>, T)->Unit

這些就組成了 SimpleAdapter 的第二個類 HolderInfo,這裏就再也不貼代碼了。佈局

SimpleAdapter 裏的實現

這是 SimpleAdapter 裏的最後一個類,和其餘 Adapter 同樣,SimpleAdapter 也封裝了一個 List<Any> 存放列表數據,泛型 Any 是爲了避免對數據類型作約束,支持不一樣類型數據的映射和渲染。性能

  • fun getItemCount(): Int 返回 list.size
  • fun getItemViewType(position: Int): Int 會根據對應位置的數據拿到映射信息 HolderInfo,返回它的 viewType
  • fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder<Any> 再根據 viewType 拿到 HolderInfo,進而拿到佈局文件和 ViewHolder 類型等數據,進而實例化出 SimpleHolder<Any>
  • fun onBindViewHolder(holder: SimpleHolder<Any>, position: Int) 觸發 SimpleHolder.onBind 和對應 HolderInfo.onCreate

如何組織 HolderInfo

拿到 HolderInfo 的方式有兩種,一種是 getItemViewType 根據當前數據拿到對應支持的 HolderInfo,另外一種就是 onCreateViewHolderonBindViewHolder 須要根據 viewType 拿到 HolderInfo

所以須要一個以數據類型 Class 爲 key,MutableList 爲 value 的 Map,value 是個列表是由於同一類型的數據可能對應多個 HolderInfo。另外一個是以 viewType 爲 key,HolderInfo 爲 value 的 SparseArray

private val infoByDataClass = HashMap<Class<Any>, MutableList<HolderInfo<Any>>>()
    private val infoByType = SparseArray<HolderInfo<Any>?>()
複製代碼

一些全局的監聽

typealias OnCreateHolder<T> = (SimpleHolder<T>)->Unit
typealias OnBindHolder<T> = (SimpleHolder<T>, T)->Unit
/** * 監聽 onCreateViewHolder */
val onCreateListeners = ArrayList<OnCreateHolder<Any>>(2)
/** * 監聽 onBindViewHolder */
val onBindListeners = ArrayList<OnBindHolder<Any>>(2)
複製代碼

一些出錯的處理

代碼有問題,直接崩潰才能讓咱們及時發現,可是在線上的崩潰會極大影響用戶體驗,出現異常狀況,咱們但願可以有必定的降級機制來處理,這裏對一些主要場景的問題提供了降級處理的方法。

/** * getItemViewType 遇到不支持的數據類型時的出錯處理,不設置會拋出異常 */
var onGetViewTypeError: ((SimpleAdapter, Int)->Int)? = null
複製代碼

SimpleAdapter 裏可能放入不支持的數據類型,致使 getItemViewType 沒法處理,這時候就會觸發這個回調,讓使用者決定返回什麼 viewType

/** * onCreateViewHolder 的出錯處理,能夠返回自定義的 SimpleHolder 來顯示錯誤信息 * 不設置或者返回 null 會從新拋出異常 */
var onCreateError: ((SimpleAdapter, Exception, ViewGroup, Int)->SimpleHolder<Any>?)? = null
複製代碼

建立 ViewHolder 時出錯可讓使用者提供一個降級的 ViewHolder,好比渲染一個顯示出錯信息的視圖等。

/** * onBindViewHolder 的出錯處理,不設置或者返回 null 會從新拋出異常 */
var onBindError: ((SimpleAdapter, Exception, SimpleHolder<Any>, Int)->Unit)? = null
複製代碼

onBindViewHolder 時出錯也提供一樣的降級處理的機會,好比把整個視圖隱藏掉。

Demo

SimpleAdapter 的使用

SimpleAdapter(list)
    .addHolderInfo(/* ... */)
    .addHolderInfo(/* ... */)
    // ...
複製代碼

SimpleAdapter 的構建很簡單,只須要傳入列表就行,以後添加須要的 HolderInfo 就能完成數據到視圖的渲染,渲染方式下面細說。

一個數據類型 + 一個佈局文件

addHolderInfo(
    HolderInfo(
        LoadingMore::class.java, R.layout.holder_loading_more ) ) 複製代碼

這是最簡單的建立 HolderInfo 的方式,會把某一類型的數據映射到佈局文件建立的視圖,因爲沒有設置任何回調,沒法變動視圖,所以只適用於視圖固定的場合。上面的配置會渲染成「正在加載......」。

一個數據類型 + 一個佈局文件 + 統一的處理方式

addHolderInfo(
    HolderInfo(
        TitleLine::class.java, R.layout.holder_title_line, onCreate = { holder ->
            holder.itemView.setOnClickListener { v ->
                val toast = holder.data?.toast ?: return@setOnClickListener
                v.context.toast(toast)
            }
        },
        onBind = { holder, data ->
            holder.v<TextView>(R.id.tv_title)?.text = data.title
            holder.v<TextView>(R.id.tv_info)?.text = data.info
        }
    )
)
複製代碼

一些簡單的卡片,不必繼承 SimpleHolder 單獨實現一個類,能夠經過相似這種方式在 onCreateonBind 中作些統一的渲染邏輯。Demo 中的「即將上線」、「熱門電影」等就是用這種方式渲染的。

使用自定義的 ViewHolder

addHolderInfo(
    HolderInfo(
        Movie::class.java,
        R.layout.holder_online_movie,
        OnlineMovieViewHolder::class.java,
        isSupport = { it.isOnline }
    )
)
複製代碼

通常卡片邏輯都比較複雜,會單獨實現一個 ViewHolder,這裏須要繼承自 SimpleAdapter。因爲一種數據類型可能渲染成多種卡片,還可能須要用 isSupport 過濾,固然 onCreateonBind 也是能夠用來處理統一邏輯的,好比設置一些 listener 等。Demo 中的在線電影、即將上映的電影和推薦的電影都是這種方式實現的。

出錯處理

onGetViewTypeError = { adapter, position ->
    sceneContext?.toast("不支持的數據: ${adapter.list[position]}")
    0
}
onCreateError = { _, _, parent, viewType ->
    val v = LayoutInflater.from(parent.context).inflate(R.layout.holder_error, parent, false)
    val holder = SimpleHolder<Any>(v)
    holder.v<TextView>(R.id.tv_info)?.text = "不支持的 viewType: $viewType"
    holder
}
複製代碼

Demo 中 onGetViewTypeError 會彈出 toast 提示,onCreateError 會顯示一個出錯的視圖。

其餘

Demo 中的佈局方式用的是 GridLayoutManager,用起來也很簡單,再也不贅述。

ListViewModel 中會處理刷新和加載更多的邏輯,這裏簡單處理了「空閒」、「刷新中」和「正在加載更多」三種狀態。

「正在加載更多」的實現,須要配置 HolderInfo 指定下渲染邏輯,在加載時往列表裏添加一個對應的數據,加載完再刪掉就行。加載更多的觸發用的是 RecyclerViewLoadMore,原理就是 RecyclerView.addOnScrollListener,再列表接近底部時觸發回調,加載更多。

列表的更新用了 LiveList,是一個封裝了列表操做和 Adapter 更新的類,就不用手動 notifyXxx 了。

結束

RecyclerView 不論是性能仍是架構設計,都很優秀,Recycler 處理緩存,LayoutManager 處理佈局,ItemDecoration 處理卡片的裝飾,ItemAnimator 處理動畫,各個類的職責分離且明確,很是值得咱們學習。SimpleAdapter 只處理數據到視圖的映射,什麼頭部卡片,底部卡片,都是數據層面的邏輯,只用變動列表數據,Adapter 只要考慮簡單高效地解決 adapter 的邏輯就行。

Demo 和工具類的庫都在 github.com/funnywolfda… 中,以後還會不斷地更新一些本身經常使用的工具,總結並補充相應 demo,依賴這個庫只須要:

allprojects {
    repositories {
        maven { url "https://jitpack.io" }
    }
}

dependencies {
    implementation 'com.github.funnywolfdadada:HollowKit:1.0'
}
複製代碼
相關文章
相關標籤/搜索