市面上已經有不少開源且好用的 Adapter
庫,爲啥非要本身造輪子呢?
java
學習和工做中我經常使用過 BaseRecyclerViewAdapterHelper 和 SugarAdapter,也都實現過複雜的列表,倒也不能說很差用或者不順手,只是和本身的理念不太匹配,一個簡單的 Adapter
爲啥都要搞得那麼複雜呢,因而決定按照本身的想法造一個簡單高效的輪子。git
Adapter
適配器,顧名思義,是兩類不一樣東西之間溝通的橋樑,RecyclerView.Adapter
則是爲了實現數據到視圖的適配,說白了就是一種 Model -> View
的映射關係。
本着「單一職責」的原則,這裏造的輪子只考慮 Adapter
的本職工做,不考慮添加什麼「頭部」、「尾部」之類的東西,簡單高效地實現 Model -> View
的映射。github
這裏先放下 Demo 中的截圖,Demo 中會有 6 種卡片樣式, 4 種數據類型和 3 種自定義的 ViewHolder。
緩存
這裏主要講述下實現的一些思路,和大體的實現,無論興趣的能夠直接看 Demo 的演示。bash
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
對某一類數據的渲染方式。因爲 RecyclerView
對 ViewHolder
緩存是以 getItemViewType
的值爲 key 的進行的,所以「一條映射關係」的配置須要與惟一的 viewType
相匹配。maven
這裏須要定義一個配置項,來配置一條映射關係,在定義以前,先來考慮下映射過程當中都涉及到哪些東西:工具
Class<T>
(T)->Boolean
LayoutRes
ViewHolder
:Class<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
,另外一種就是 onCreateViewHolder
和 onBindViewHolder
須要根據 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
時出錯也提供一樣的降級處理的機會,好比把整個視圖隱藏掉。
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
單獨實現一個類,能夠經過相似這種方式在 onCreate
和 onBind
中作些統一的渲染邏輯。Demo 中的「即將上線」、「熱門電影」等就是用這種方式渲染的。
ViewHolder
addHolderInfo(
HolderInfo(
Movie::class.java,
R.layout.holder_online_movie,
OnlineMovieViewHolder::class.java,
isSupport = { it.isOnline }
)
)
複製代碼
通常卡片邏輯都比較複雜,會單獨實現一個 ViewHolder
,這裏須要繼承自 SimpleAdapter
。因爲一種數據類型可能渲染成多種卡片,還可能須要用 isSupport
過濾,固然 onCreate
和 onBind
也是能夠用來處理統一邏輯的,好比設置一些 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'
}
複製代碼