場景
最近在開發(fā)產(chǎn)品的過程中,有這樣一個場景:頁面包含一個豎向列表钉赁,當(dāng)點擊特定的item時會在其中展示建議視圖昧互。這些建議視圖從服務(wù)器獲取并展示在一個橫向滑動的列表中荤西。我們還是如常使用RecyclerView去做。
技術(shù)規(guī)劃
現(xiàn)在讓我們深入到技術(shù)細節(jié)退敦,獲取建議并展示的邏輯是在一個單獨的view holder中處理粘咖,其內(nèi)部包含它自己的RecyclerView去展示橫向的列表。在onBindView中侈百,這個view holder執(zhí)行獲取數(shù)據(jù)和在RecyclerView中展示的邏輯瓮下。我們不能在不知道用戶點擊具體哪個item就去提前獲取數(shù)據(jù),而且我們也不能把數(shù)據(jù)保存到父級adapter中钝域,就結(jié)構(gòu)層面來說我們想要保證這些view holder邏輯是分離的讽坏。
出現(xiàn)的問題
當(dāng)我們每次滑動列表到這個view holder顯示在屏幕中時,onBind會被調(diào)用并且我們不得不重新獲取數(shù)據(jù)并配置新的adapter然后展示到UI上例证。
解決方案
如果我們能禁止這個建議view holder每次都調(diào)用onBind就沒問題了 路呜。為了達到這個目的,我們需要做兩件事织咧。首先拣宰,我們需要告訴RecyclerView始終重用同一個view holder,同一位置不再調(diào)用onBind.其次烦感,相同的view Type在不同的位置上不能使用這個view holder巡社。
簡而言之,我們需要一個可以緩存特定類型視圖的解決方案手趣。
不太妥帖的方案
1.我們可以增加RecyclerView的緩存數(shù)量晌该。但是肥荔,這樣的話我們就必須把緩存數(shù)量設(shè)置成跟整個列表的數(shù)量一致,這是因為RecyclerView只能緩存我們可見區(qū)域內(nèi)的視圖朝群,所以如果你的view holder遠離可見區(qū)域它將不會被緩存燕耿,直到緩存大小等于整個列表。
但是姜胖,這種方案對內(nèi)存是不友好的誉帅,當(dāng)你將整個列表都緩存起來的話,使用RecyclerView也就沒有意義了右莱。
2.其次蚜锨,有人會建議你去將特定的view holder定義成不可復(fù)用。
holder.setIsRecyclable(false);
或是
recyclerView.getRecycledViewPool()
.setMaxRecycledViews(TARGET_VIEW_HOLDER, POOL_CAPACITY);
//即設(shè)置 POOL_CAPACITY 為 0.
它將幫助你實現(xiàn)第二點即對相同的類型且不同位置不去使用view holder慢蜓,不會將它移動到RecyclerViewPool亚再。但是,它不會在所有情況下都滿足我們的目的晨抡,因為氛悬,當(dāng)我們將目標(biāo)view holder從可見區(qū)域移開時,RecyclerView仍然會從cache中清除它耘柱。它只是沒有移到RecyclerViewPool中如捅。
進階方案
RecyclerView提供了一個回調(diào)方法:
ViewCacheExtension()
RecyclerView在檢查了它自身的cache或scrap view之后以及在將其移動到RecyclerViewPool之前會調(diào)用它的方法 getViewForPositionAndType 。這是一個抽象類调煎,你可以實現(xiàn)它并返回特定位置的視圖伪朽。
val viewCacheExtension = object : ViewCacheExtension() {
override fun getViewForPositionAndType(
recycler: Recycler,
position: Int,
type: Int
): View? {
}
}
我們可以利用它來緩存特定的視圖類型。
讓我們看看如何使用它來達到我們的目的汛蝙,并創(chuàng)建一個通用的解決方案烈涮,使它允許為特定的視圖類型啟用緩存。
1.選取特定的view holder去緩存
我們需要一個存儲view holder的Map用來保存這些已經(jīng)緩存的視圖窖剑。我們還需要存儲需要啟用緩存的視圖holder類型坚洽,這里我們使用Set。
val cachedItems: MutableMap<Long, ViewHolder> = HashMap()
//這里的Key是唯一的用來標(biāo)識特定位置的item
val cachedViewHolderTypes: MutableSet<Int> = HashSet()
現(xiàn)在填充已經(jīng)創(chuàng)建的視圖的最佳時機是:
onViewDetachedFromWindow()
這是因為視圖已經(jīng)被創(chuàng)建并且已經(jīng)被綁定了一次西土,在它被回收之前讶舰,我們將它保存到我們的Map中:
//一旦視圖劃出屏幕便將其保存起來
override fun onViewDetachedFromWindow(holder: ViewHolder) {
if (cachedViewHolderTypes.contains(holder.itemViewType)) {
val pos = holder.bindingAdapterPosition
Timber.d("onViewDetached called for position : $pos")
if (pos > -1) {
holder.setIsRecyclable(false)
cachedItems[getCachedItemId(pos)] = holder
}
}
super.onViewDetachedFromWindow(holder)
}
這里,我們存儲的view holder與其Item Id相關(guān)聯(lián)需了,即RecyclerView的getItemId(int pos)方法跳昼。
可能你也注意到了我們將這個holder置為可被回收,否則相同的holder將被我們重用到其他的位置上肋乍,因為一旦劃出屏幕RecyclerView便將其移動到RecyclerView Pool中鹅颊。
2.在onBindView調(diào)用之前返回已緩存的view holder
對此,我們像這樣使用ViewCacheExtension:
//返回特定位置已緩存的view holder
private val viewCacheExtension = object : ViewCacheExtension() {
override fun getViewForPositionAndType(
recycler: Recycler,
position: Int,
type: Int
): View? {
val cachedItemId = getItemId(position)
if (cachedViewHolderTypes.contains(type) && cachedItemId != NO_CACHED_ITEM_ID
&& cachedItems.containsKey(cachedItemId)
) {
Timber.d("Returning view from custom cache for position:$position")
return cachedItems[cachedItemId]?.itemView
}
return null
}
}
在返回已緩存的視圖之前需要做相關(guān)的檢查:
- 通過對集合進行查詢墓造,我們檢查對應(yīng)的view holder類型是否已啟用緩存堪伍。
- 是否這個holder的item id 和當(dāng)前指向的位置相同锚烦。這是為了確保在我們的adapter已經(jīng)被新的數(shù)據(jù)更新了但是我們的緩存Map還持有舊的view holder的場景不會發(fā)生。這是重要的帝雇,因為在數(shù)據(jù)集更新后涮俄,新的視圖類型被分配到這個位置上,但是尸闸,我們?nèi)匀粸檫@個位置返回舊的view holder彻亲,這將導(dǎo)致仍顯示舊的視圖。
3.確保在數(shù)據(jù)集變更的同時緩存的視圖也會被更新
為了達到當(dāng)數(shù)據(jù)集有變化吮廉,我們就能更新緩存Map的目的苞尝,我們需要利用AdapterDataObserver 觀察數(shù)據(jù)的變化。這樣當(dāng)有任何通知到來我們會得到回調(diào)茧痕。
我們可以將數(shù)據(jù)集變更調(diào)用主要分為兩組來處理:
- 使用notifyItemRangeRemoved()移除的緩存項。
- 我們在notifyItemRangeMoved()或notifyDataSetChanged()之后移除的view holder恼除。
為了簡化這個數(shù)據(jù)集變化導(dǎo)致的分組踪旷,我們繼承AdapterDataObserver類來實現(xiàn)一個抽象類。
這里包含兩個回調(diào)方法
- onChanged() : 我們觀察任何數(shù)據(jù)集的變化除了數(shù)據(jù)被移除這個操作外豁辉。所以令野,我們只是檢查,如果我們的緩存項是否仍然存在于新的集合中徽级。
如果緩存項不在新的集合中气破,這個觀察器將負責(zé)刪除它們。
override fun onChanged() {
Timber.d("items added or moved")
if (cachedItems.isEmpty())
return
val iterator: MutableIterator<String> = cachedItems.keys.iterator()
val newItemsIds = mutableSetOf<String>()
for (newIndex in 0 until itemCount) {
val itemId = getItemId(newIndex)
newItemsIds.add(itemId)
}
//移除新集合中沒有的緩存項
while (iterator.hasNext()) {
val cachedItemId = iterator.next()
val holder = cachedItems[cachedItemId]
if (holder?.bindingAdapterPosition == -1 || !newItemsIds.contains(cachedItemId)) {
holder?.itemView?.let { view ->
if (view.isAttachedToWindow || view.parent != null) {
recyclerView?.removeView(view)
view.visibility = View.GONE
}
}
iterator.remove()
}
}
}
- onItemRangeRemoved(positionStart: Int, itemCount: Int) :這里餐抢,我們觀察數(shù)據(jù)的刪除操作现使。
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
Timber.d("On Item range removed called for adapter observer")
if (cachedItems.isEmpty())
return
val iterator: MutableIterator<String> = cachedItems.keys.iterator()
while (iterator.hasNext()) {
val cachedItemId = iterator.next()
val index = cachedItems[cachedItemId]?.bindingAdapterPosition ?: -1
if (index != -1 && positionStart <= index && index < positionStart + itemCount) {
Timber.d("Removing view from adapter observer for position:$index")
val holder = cachedItems[cachedItemId]
holder?.itemView?.let { view ->
if (view.isAttachedToWindow || view.parent != null) {
recyclerView?.removeView(view)
view.visibility = View.GONE
}
}
iterator.remove()
}
}
}
4.在RecyclerView中注冊這些回調(diào)方法
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
registerAdapterDataObserver(adapterDataObserver)
recyclerView.setViewCacheExtension(viewCacheExtension)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
unregisterAdapterDataObserver(adapterDataObserver)
super.onDetachedFromRecyclerView(recyclerView)
}
5.一個簡化的方法去為特定的view holder類型啟動緩存
fun enableCacheForViewHolderType(type: Int) {
cachedViewHolderTypes.add(type)
}
到這里我們已經(jīng)準(zhǔn)備好了,我們自己實現(xiàn)的為RecyclerView特定view holder類型進行緩存的邏輯旷痕,只需要在onCreateViewHolder ()加入一行代碼即可碳锈。
enableCacheForViewHolderType(viewType);
現(xiàn)在,在被聲明view holder類型的onBindView方法只會執(zhí)行一次欺抗。
完整代碼:
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.*
import timber.log.Timber
import java.util.HashSet
import java.util.concurrent.ConcurrentHashMap
/**
* Created by Sumeet on 24,January,2022
This class helps you to return same view holder once bind , without rebinding again
Just sent the required type for enabling cache using enableCacheForViewHolderType and overriding getItemId()
*/
abstract class BaseAdapterWithCaching : RecyclerView.Adapter<ViewHolder>() {
private val cachedItems: MutableMap<String, ViewHolder> = ConcurrentHashMap()
private val cachedViewHolderTypes: MutableSet<Int> = HashSet()
companion object {
const val NO_CACHED_ITEM_ID = "NO_CACHED_ITEM_ID"
}
//Return cached view holder for particular position
private val viewCacheExtension = object : ViewCacheExtension() {
override fun getViewForPositionAndType(
recycler: Recycler,
position: Int,
type: Int
): View? {
val cachedItemId = getItemId(position)
if (cachedViewHolderTypes.contains(type) && cachedItemId != NO_CACHED_ITEM_ID
&& cachedItems.containsKey(cachedItemId)
) {
Timber.d("Returning view from custom cache for position:$position")
return cachedItems[cachedItemId]?.itemView
}
return null
}
}
fun enableCacheForViewHolderType(type: Int) {
cachedViewHolderTypes.add(type)
}
protected fun isCacheEnabledForViewHolder(type: Int): Boolean =
cachedViewHolderTypes.contains(type)
private val adapterDataObserver: DataChangeObserver = object :
DataChangeObserver() { //This observers will take care of removing the cached items if they are not in new set
override fun onChanged() {
Timber.d("items added or moved")
if (cachedItems.isEmpty())
return
val iterator: MutableIterator<String> = cachedItems.keys.iterator()
val newItemsIds = mutableSetOf<String>()
for (newIndex in 0 until itemCount) {
val itemId = getItemId(newIndex)
newItemsIds.add(itemId)
}
//Remove cached items which are not in new set
while (iterator.hasNext()) {
val cachedItemId = iterator.next()
val holder = cachedItems[cachedItemId]
if (holder?.bindingAdapterPosition == -1 || !newItemsIds.contains(cachedItemId)) {
holder?.itemView?.let { view ->
if (view.isAttachedToWindow || view.parent != null) {
recyclerView?.removeView(view)
view.visibility = View.GONE
}
}
iterator.remove()
}
}
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
Timber.d("On Item range removed called for adapter observer")
if (cachedItems.isEmpty())
return
val iterator: MutableIterator<String> = cachedItems.keys.iterator()
while (iterator.hasNext()) {
val cachedItemId = iterator.next()
val index = cachedItems[cachedItemId]?.bindingAdapterPosition ?: -1
if (index != -1 && positionStart <= index && index < positionStart + itemCount) {
Timber.d("Removing view from adapter observer for position:$index")
val holder = cachedItems[cachedItemId]
holder?.itemView?.let { view ->
if (view.isAttachedToWindow || view.parent != null) {
recyclerView?.removeView(view)
view.visibility = View.GONE
}
}
iterator.remove()
}
}
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
registerAdapterDataObserver(adapterDataObserver)
recyclerView.setViewCacheExtension(viewCacheExtension)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
Timber.d("onDetached recycler view called ")
unregisterAdapterDataObserver(adapterDataObserver)
super.onDetachedFromRecyclerView(recyclerView)
}
//Saving the cached item once it goes out of screen
override fun onViewDetachedFromWindow(holder: ViewHolder) {
if (cachedViewHolderTypes.contains(holder.itemViewType)) {
val pos = holder.bindingAdapterPosition
Timber.d("onViewDetached called for position : $pos")
if (pos > -1) {
holder.setIsRecyclable(false)
cachedItems[getItemId(pos)] = holder
}
}
super.onViewDetachedFromWindow(holder)
}
private abstract class DataChangeObserver : AdapterDataObserver() {
abstract override fun onChanged()
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeChanged(
positionStart: Int, itemCount: Int,
payload: Any?
) {
onChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
onChanged()
}
}
}