RecyclerView的刷新基本分為以下兩種情況:
- 1. 如果大量的數(shù)據(jù)被修改或者被修改數(shù)據(jù)的位置不確定继准,
這個方法很消耗性能,不到萬不得已不要使用
棘街,請盡量使用下面的刷新方法蟆盐。實(shí)現(xiàn)如下:
adapter.notifyDataSetChanged();
- 2. 刷新某一項(xiàng),定點(diǎn)刷新(常用)遭殉,
消耗性能很少石挂,但是會有定位的問題,可能在過程中需要遍歷集合獲取操作下標(biāo)
//刷新某Item中的所有組件
adapter.notifyItemChanged(position);
//刷新某Item中的部分組件
adapter.notifyItemChanged(position, payloads);
//插入Item
adapter.notifyItemInserted(position);
//刪除Item
adapter.notifyItemRemoved(position);
//移動Item
adapter.notifyItemMoved(position, position + 1);
RecyclerView的刷新問題Google推出了DiffUtil這個解決方案:
-
DiffUtil的運(yùn)用邏輯非常簡單险污,大致如下:
實(shí)現(xiàn)對比新舊數(shù)據(jù)的方法(類似比較器)痹愚,這樣DiffUtil便知道當(dāng)新數(shù)據(jù)來臨時,該不該更新某個item蛔糯。
更新數(shù)據(jù)時拯腮,把新舊數(shù)據(jù)丟給DiffUtil,底層會根據(jù)你實(shí)現(xiàn)的對比方法蚁飒,利用一種差分算法自動計算出差異动壤,最后局部更新到UI。 -
DiffUtil的使用也很簡單:
1淮逻、先實(shí)現(xiàn)比較新舊數(shù)據(jù)的回調(diào)琼懊,可以是一個獨(dú)立的類,也可以寫成Adapter的內(nèi)部類:
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
// ...
private class DiffCallback extends DiffUtil.Callback {
private List<T> oldData, newData;
DiffCallback(List<T> oldData, List<T> newData) {
this.oldData = oldData;
this.newData = newData;
}
@Override
public int getOldListSize() {
return oldData.size();
}
@Override
public int getNewListSize() {
return newData.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
T oldT = oldData.get(oldItemPosition);
T newT = newData.get(newItemPosition);
// 實(shí)際情況最好是在此處對比新舊數(shù)據(jù)的id(比如用戶uid)弦蹂,這里為了方便示例直接equals對象了
// 若此處返回true肩碟,則DiffUtil會再調(diào)用下面的areContentsTheSame方法,進(jìn)一步對比UI是否有變化
// 若此處返回false凸椿,則說明id都不同削祈,肯定不是一個item
return Objects.equals(oldT, newT);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
// TODO 比較新舊數(shù)據(jù)(主要是UI展示內(nèi)容)是否相同,這里為了方便示例直接返回true
return true;
}
}
}
2脑漫、然后在Adapter內(nèi)部實(shí)現(xiàn)一個update數(shù)據(jù)的方法:
@Override
public void updateData(List<T> newData) {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffCallback(getData(), newData));
// 這里的getData即表示獲取整個列表的數(shù)據(jù)髓抑,自行實(shí)現(xiàn)即可
getData().clear();
getData().addAll(newData);
result.dispatchUpdatesTo(this);
}
3、重點(diǎn)還是 areItemsTheSame 和 areContentsTheSame 方法优幸,后者大部分時候只需要對比每個item上UI展示出來的數(shù)據(jù)即可吨拍,因?yàn)橛脩糁魂P(guān)心眼見的內(nèi)容。
解決使用后產(chǎn)生的問題:
我們會發(fā)現(xiàn)在上面的使用示例中网杆,updateData 方法內(nèi)部對原數(shù)據(jù)進(jìn)行了清除和添加的操作羹饰,這會導(dǎo)致一個問題便是:列表數(shù)據(jù)集合中的對象已經(jīng)變了伊滋,即使其某項(xiàng)對應(yīng)的UI內(nèi)容沒有發(fā)生變化。
舉個例子队秩,一個通訊錄列表里面有 [小明, 小紅] 兩個人笑旺,對應(yīng)內(nèi)存地址為 [a1, a2],現(xiàn)在通過上述 updateData 方法更新了通訊錄列表馍资,UI內(nèi)容變成了 [小王, 小紅]筒主,對應(yīng)內(nèi)存地址為 [b1, b2]。對用戶來說小紅這個item看上去沒有發(fā)生變化鸟蟹,但其實(shí)對應(yīng)的數(shù)據(jù)類對象已經(jīng)不同乌妙。而且此時 onBindViewHolder 方法只會觸發(fā)一次,將小明更新成小王建钥,而不會觸發(fā)小紅那個position對應(yīng)的 onBindViewHolder 藤韵。
上述細(xì)節(jié)很關(guān)鍵,如果開發(fā)過程中綁定(bind)數(shù)據(jù)不恰當(dāng)?shù)脑捫芫腿菀自斐筛鞣N奇異問題荠察,比如網(wǎng)上資料最多的DiffUtil導(dǎo)致item點(diǎn)擊事件數(shù)據(jù)錯位問題、數(shù)組越界崩潰問題等等奈搜。
這里的“不恰當(dāng)”,絕大部分情況下盯荤,總結(jié)出來:其實(shí)指的就是在 onBindViewHolder 方法中持有了某個位置(position)對應(yīng)數(shù)據(jù)的不可變對象馋吗。最常見的誤用示例就是在 onBindViewHolder 中設(shè)置某些控件的點(diǎn)擊事件并引用數(shù)據(jù)對象:
// 此處假設(shè)item的數(shù)據(jù)類為User
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
MyItemViewHolder h = (MyItemViewHolder) holder;
User user = getData().get(position);
h.mNameTextView.setOnClickListener(v -> {
// 第2種寫法:User user = getData().get(position);
// 假設(shè)這里是點(diǎn)擊item跳轉(zhuǎn)到該User對應(yīng)的個人主頁界面
startWebView(user.getHomePageUrl());
});
}
在不接入DiffUtil之前,上面這段代碼沒有任何問題秋秤,因?yàn)槲覀兌际鞘褂?notifyDataSetChanged 方法來更新UI宏粤,每次更新調(diào)用到 onBindViewHolder 時,點(diǎn)擊事件都會重新設(shè)置灼卢,get出來的user對象自然也是最新的绍哎。一旦我們使用了DiffUtil,就會出問題了鞋真。
回到上面小王綠了小明的例子崇堰,在我們的 updateData 方法執(zhí)行后,如果我們只對比了user的名字這個屬性(其實(shí)也只需要對比這個屬性)涩咖,那么小紅那一個item就不會觸發(fā)對應(yīng)的 onBindViewHolder 海诲,即小紅的點(diǎn)擊事件回調(diào)里,仍然持有著舊數(shù)據(jù)集的user對象(對應(yīng)那個內(nèi)存地址a2)檩互。但實(shí)際上小紅應(yīng)該對應(yīng) b2 那個內(nèi)存了特幔,這就造成 a2 內(nèi)存無法釋放,問題是不是顯得有點(diǎn)嚴(yán)重了闸昨。
有同學(xué)說無所謂呀蚯斯,反正點(diǎn)擊事件依然有效薄风。那如果我說網(wǎng)絡(luò)數(shù)據(jù)刷新下來小紅的 homePageUrl 變了呢?是不是還得把這個屬性加入DiffUtil的對比方法中拍嵌?這樣最終會導(dǎo)致小紅的 onBindViewHolder 方法也執(zhí)行遭赂,跟 notifyDataSetChanged 豈不是沒什么兩樣了?
此外撰茎,若get對象寫成注釋中的第2種寫法嵌牺,且列表第0個位置的item被刪了呢?小紅頂上去變成了第0個龄糊,此時由于小紅的UI內(nèi)容沒變逆粹,只是位置變了,所以 onBindViewHolder 依然不會執(zhí)行炫惩。以上面的示例代碼來看僻弹,當(dāng)再次點(diǎn)擊小紅時,就會直接出現(xiàn)數(shù)組越界的異常他嚷。因?yàn)閜osition還是之前的1蹋绽,而此時小紅的position已經(jīng)為0。
顯然上述出現(xiàn)的這些問題不符合谷歌的設(shè)計初衷筋蓖,也不符合我們使用DiffUtil的初衷卸耘。其實(shí)解決辦法很簡單,就是要對 onBindViewHolder 方法有一個正確的認(rèn)知粘咖,其原則就是:
- onBindViewHolder 只做UI內(nèi)容的更新蚣抗,如 setText,setImageXXX 等方法瓮下。做到數(shù)據(jù)對象一次性使用翰铡。
- 不要跨作用域持有與位置(position)相關(guān)的數(shù)據(jù),比如每個item的數(shù)據(jù)對象讽坏。尤其就是避免在 onBindViewHolder 中設(shè)置點(diǎn)擊事件監(jiān)聽锭魔。
正確的點(diǎn)擊事件監(jiān)聽還是參照如下形式比較好:
// 比如這是某個Base適配器類
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
// ...
private View.OnClickListener mOnClickListener;
private View.OnLongClickListener mOnLongClickListener;
private OnItemClickListener mOnItemClickListener;
public interface OnItemClickListener {
void onItemClick(View view, RecyclerView.ViewHolder holder, int position);
void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position);
}
public BaseXXXAdapter(Context context) {
// ...
mOnClickListener = v -> {
RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
int pos = h.getAdapterPosition();
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(v, h, pos);
}
};
mOnLongClickListener = v -> {
RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
int pos = h.getAdapterPosition();
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemLongClick(v, h, pos);
}
return true;
};
}
public void setOnItemClickListener(OnItemClickListener clickListener) {
this.mOnItemClickListener = clickListener;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// ...省略holder實(shí)例化
holder.itemView.setTag(holder); // 把holder當(dāng)tag存
holder.itemView.setOnClickListener(mOnClickListener);
holder.itemView.setOnLongClickListener(mOnLongClickListener);
return holder;
}
}
// 繼承實(shí)現(xiàn)的實(shí)際業(yè)務(wù)Adapter
public class XXXAdapter extends BaseXXXAdapter<User> {
public XXXAdapter(Context context) {
setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) {
MyItemViewHolder h = (MyItemViewHolder) holder;
// 每次點(diǎn)擊都保證了為對應(yīng)位置的數(shù)據(jù),再也不用擔(dān)心數(shù)據(jù)錯位問題了
User user = getData().get(position);
}
@Override
public void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) {
// ...
}
});
}
}
參考來源:掘金 作者:針葉
參考來源:簡書 作者:BruceBug