閱讀了大神寫的代碼,才知道每一行都不是白寫的,寫的有理有據(jù),還很優(yōu)雅。膜拜....
一、作用
可以計算兩個 List 之間的差異,得到兩個 List 之間的差異集,如果 List 集合很大,計算兩個 List 之間的差異耗時,應該放到子線程中執(zhí)行,計算得到 DiffUtil.DiffResult 后,將該結(jié)果集應用到主線程的 RecyclerView 上。
二、相關(guān)概念
1. 相關(guān)類
(1)DiffUtil.Callback
計算兩個 List 之間的差異時,由 DiffUtil 調(diào)用,
(2)DiffUtil.ItemCallback
用于計算 List 中兩個 non-null Item 的差異
(3)DiffUtil.DiffResult
保存了
DiffUtil.calculateDiff(callback,boolean)
的返回結(jié)果
2. 相關(guān)方法
(1)static DiffUtil.calculateDiff(DiffUtil.Callback cb)
(2)static DiffUtil.calculateDiff(DiffUtil.Callback cb,boolean detectMoves)
如果 old 和 new List 以相同的規(guī)則進行過排序,并且 Item 從不會移動(改變位置),那么,可以禁用 detectMoves=false,提高計算效率
三、使用
1. Item 實體類
項目中使用這個的場景可能就是:老數(shù)據(jù)已經(jīng)填充好了 Adapter,這時又從網(wǎng)絡中拉取了新數(shù)據(jù),那么使用 DiffUtil 比較兩個數(shù)據(jù)集的差異,將修改應用到 Adapter。此處為了復用舊數(shù)據(jù)源模擬新的數(shù)據(jù)集,所以為其實現(xiàn)
Clonable
接口。
public class User implements Cloneable {
private int id;
private String name;
private int age;
private String profile;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getProfile() {
return profile;
}
public void setProfile(String profile) {
this.profile = profile;
}
public User(int id, String name, int age, String profile) {
this.id = id;
this.name = name;
this.age = age;
this.profile = profile;
}
@NonNull
@Override
public User clone() {
User o = null;
try {
o = (User) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
2. 實現(xiàn)一個普通的 Adapter
繼承
RecyclerView.Adapter
,實現(xiàn)相關(guān)的抽象方法創(chuàng)建 ViewHolder,繼承自
RecyclerView.ViewHolder
在 Adapter 中保存數(shù)據(jù)源、上下文等
public class MyDiffAdapter extends RecyclerView.Adapter < MyDiffAdapter.MyTicketViewHolder > {
private List < User > mData;
private Context mContext;
private LayoutInflater mLayoutInflater;
public MyDiffAdapter(List < User > data, Context context) {
mData = data;
mContext = context;
mLayoutInflater = LayoutInflater.from(context);
}
public List < User > getData() {
return mData;
}
public void setData(List < User > data) {
mData = data;
}
@NonNull
@Override
public MyTicketViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = mLayoutInflater.inflate(R.layout.user_item, parent, false);
return new MyTicketViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position) {
User user = mData.get(position);
// 為控件綁定數(shù)據(jù)
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
class MyTicketViewHolder extends RecyclerView.ViewHolder {
public MyTicketViewHolder(@NonNull View itemView) {
super(itemView);
}
}
}
3. 為 Adapter 設置好初始數(shù)據(jù)源,先讓它跑起來哈~
設置數(shù)據(jù)集時可以先進行排序,防止顯示亂序
private void initViews() {
mRecyclerView = findViewById(R.id.user_rv);
mRefreshBtn = findViewById(R.id.btn_refresh);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 1,創(chuàng)建Adapter
List < User > data = initData();
mAdapter = new MyDiffAdapter(data, this);
// 2,為RecyclerView設置適配器
mRecyclerView.setAdapter(mAdapter);
}
private List < User > initData() {
List < User > data = new ArrayList < > ();
data.add(new User(1, "福子", 10, "adfada"));
data.add(new User(2, "大牛", 10, "adfada"));
data.add(new User(1, "栓子", 10, "adfada"));
data.add(new User(4, "鐵柱", 10, "adfada"));
data.add(new User(5, "鋼蛋", 10, "adfada"));
return data;
}
4. DiffUtil 的簡單使用
模擬從網(wǎng)絡加載新的數(shù)據(jù)源,然后設置給 Adapter。
創(chuàng)建自己的 DiffUtil.Callback,定義自己的 Item 比較規(guī)則。
public class MyDiffCallback extends DiffUtil.Callback {
private List < User > oldData;
private List < User > newData;
// 這里通過構(gòu)造函數(shù)把新老數(shù)據(jù)集傳進來
public MyDiffCallback(List < User > oldData, List < User > newData) {
this.oldData = oldData;
this.newData = newData;
}
@Override
public int getOldListSize() {
return oldData == null ? 0 : oldData.size();
}
@Override
public int getNewListSize() {
return newData == null ? 0 : newData.size();
}
// 判斷是不是同一個Item:如果Item有唯一標志的Id的話,建議此處判斷id
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
User oldUser = oldData.get(oldItemPosition);
User newUser = newData.get(newItemPosition);
return oldUser.getId() == newUser.getId();
}
// 判斷兩個Item的內(nèi)容是否相同
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
// 默認內(nèi)容是相同的,只要有一項不同,則返回false
User oldUser = oldData.get(oldItemPosition);
User newUser = newData.get(newItemPosition);
// name
if (!oldUser.getName().equals(newUser.getName())) {
return false;
}
// age
if (oldUser.getAge() != newUser.getAge()) {
return false;
}
// profile
if (!oldUser.getProfile().equals(newUser.getProfile())) {
return false;
}
return true;
}
}
此處添加一個按鈕,模擬從網(wǎng)絡上獲取數(shù)據(jù)后刷新列表的操作。利用 DiffUtil 計算新老數(shù)據(jù)集的差異,并將差異應用到 Adapter 上。
private void initListener() {
mRefreshBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
refreshData();
}
});
}
private void refreshData() {
// 新的數(shù)據(jù)源
List < User > oldData = mAdapter.getData();
List < User > newData = new ArrayList < > ();
for (int i = 0; i < oldData.size(); i++) {
newData.add(oldData.get(i).clone());
}
// 模擬新增數(shù)據(jù)
newData.add(new User(6, "趙子龍", 100, "一個神人"));
// 模擬數(shù)據(jù)修改
newData.get(0).setName("福子222");
newData.get(0).setProfile("這是一個有福的女子");
// 模擬數(shù)據(jù)移位
User user = newData.get(1);
newData.remove(user);
newData.add(user);
// 1,首先將新數(shù)據(jù)集設置給Adapter
mAdapter.setData(newData);
// 2,計算新老數(shù)據(jù)集差異,將差異更新到Adapter
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback(oldData,newData));
diffResult.dispatchUpdatesTo(mAdapter);
}
此處 DiffUtil 計算新老數(shù)據(jù)集的差異,然后根據(jù)差異自動調(diào)用以下4個方法,實現(xiàn) Item 的定向刷新。
adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);
注意:要記得先把新的數(shù)據(jù)源設置給 Adapter,然后將新老數(shù)據(jù)集的差異更新到 Adapter。因為 Adapter 更新數(shù)據(jù)時可能會用到新數(shù)據(jù)集中的數(shù)據(jù)(這個后面的高級用法中會提到)。
// 1,首先將新數(shù)據(jù)集設置給Adapter
mAdapter.setData(newData);
// 2,計算新老數(shù)據(jù)集差異,將差異更新到Adapter
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback());
diffResult.dispatchUpdatesTo(mAdapter);
缺點:例如
newData.get(0).setName("福子222"); newData.get(0).setProfile("這是一個有福的女子");
中,我明明只想修改2個字段的值,卻給我刷新了整個 Item 。所以還是有改進空間的,下面實現(xiàn)RecyclerView 的部分綁定。
5. DiffUtil 的高級用法——整個數(shù)據(jù)源發(fā)生改變時的部分綁定
雖然數(shù)據(jù)源發(fā)生改變了,但還是可以做到部分綁定,只更新個別控件。核心思想:重寫 DiffUtil.Callback 中的
public Object getChangePayload(int oldItemPosition, int newItemPosition)
方法,并配合 Adapter 中3個參數(shù)的public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position, @NonNull List<Object> payloads)
。
DiffUtil.Callback 中重寫getChangePayload()
方法
public static final String KEY_NAME = "name";
public static final String KEY_AGE = "age";
public static final String KEY_PROFILE = "profile";
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
User oldUser = oldData.get(oldItemPosition);
User newUser = newData.get(newItemPosition);
// 這里就不用比較核心字段 id 了,因為id不相同也不可能走到這一步
Bundle payload = new Bundle();
// name
if (!oldUser.getName().equals(newUser.getName())) {
payload.putString(KEY_NAME, newUser.getName());
}
// age
if (oldUser.getAge() != newUser.getAge()) {
payload.putInt(KEY_AGE, newUser.getAge());
}
// profile
if (!oldUser.getProfile().equals(newUser.getProfile())) {
payload.putString(KEY_PROFILE, newUser.getProfile());
}
if (payload.size() == 0) {
// 如果沒有變化就傳空
return null;
}
return payload;
}
Adapter 中重寫onBindViewHolder()
,完成助攻。
@Override
public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position, @NonNull List < Object > payloads) {
// payload 不會為null,但可能為empty
if (payloads.isEmpty()) {
// 如果payload是空的,那就進行一次 full bind
onBindViewHolder(holder, position);
} else {
Bundle bundle = (Bundle) payloads.get(0);
User user = mData.get(position);
for (String key: bundle.keySet()) {
switch (key) {
case KEY_NAME:
// 局部更新名字:這里可以用 payload 里面的數(shù)據(jù),不過 mData 中的數(shù)據(jù)也是新的,也可以用
holder.nameTv.setText(user.getName());
break;
case KEY_AGE:
holder.ageTv.setText(user.getAge() + "");
break;
case KEY_PROFILE:
holder.profileTv.setText(user.getProfile());
break;
default:
break;
}
}
}
}
6. DiffUtil 的高級用法——明確已知某個 Item 發(fā)生改變時的部分綁定
上面說的是整個數(shù)據(jù)源發(fā)生變化了該怎么做實現(xiàn)部分綁定,但如果我明確的知道某個 position 的 item 發(fā)生了改變的話,不可能重新構(gòu)造個數(shù)據(jù)源進行刷新吧,別急且聽下文分解。
核心是:首先更新被選中 Item 的數(shù)據(jù)源,然后把修改的內(nèi)容放到 payload 中,調(diào)用notifyItemChange()
方法更新 Item 時把 payload 傳入,接下來會回調(diào)到public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position, @NonNull List<Object> payloads)
中,實現(xiàn)部分綁定。
// item 點擊事件:假設點擊以后name會變
private void onItemClick(int position) {
// 1,更新item的數(shù)據(jù)源
User user = mAdapter.getData().get(position);
String newName = "新的張無忌";
user.setName(newName);
// 2, 傳遞一個 payload
Bundle payload = new Bundle();
payload.putString(KEY_NAME, newName);
mAdapter.notifyItemChanged(position, payload);
}
四、原理
三中5、6對整個數(shù)據(jù)源/單個 item 進行局部刷新,是有原理可追尋的。
1. DiffUtil 的高級用法——整個數(shù)據(jù)源發(fā)生改變時的部分綁定
(1)diffResult.dispatchUpatesTo(mAdaptetr)
DiffUtil.DiffResult.dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter)
/**
* 將更新事件分發(fā)到給定的Adapter
* <p>
* 例如:你有一個{@link RecyclerView.Adapter Adapter},這個Adapter有一個{@link List}數(shù)據(jù)源
* 你可以先將新的數(shù)據(jù)源賦給Adapter,然后調(diào)用該發(fā)方法將所有更新分發(fā)到RecyclerView
* <pre>
* List oldList = mAdapter.getData();
* DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
* mAdapter.setData(newList);
* result.dispatchUpdatesTo(mAdapter);
* </pre>
* <p>
* 注意:RecyclerView要求在你更改數(shù)據(jù)源后立即將更新分發(fā)到Adapter Note that the RecyclerView requires you to dispatch adapter updates immediately when you
* <p>
* @param adapter :適配器,正在顯示舊數(shù)據(jù),即將顯示新數(shù)據(jù)。
* @see AdapterListUpdateCallback
*/
public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) {
dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}
AdapterListUpdateCallback.class
/**
* ListUpdateCallback that dispatches update events to the given adapter.
* 將更新事件分發(fā)給給定 Adapter
* @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
*/
public final class AdapterListUpdateCallback implements ListUpdateCallback {
@NonNull
private final RecyclerView.Adapter mAdapter;
/**
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
*
* @param adapter The Adapter to send updates to.
*/
public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
/**
* Called when {@code count} number of items are inserted at the given position.
* 當在position位置插入count個Item時調(diào)用
* @param position The position of the new item.
* @param count The number of items that have been added.
*/
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
/**
* Called when {@code count} number of items are removed from the given position.
*position位置的count個Item被刪除
* @param position The position of the item which has been removed.
* @param count The number of items which have been removed.
*/
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
/**
* Called when an item changes its position in the list.
* 當一個item改變了它的position時調(diào)用
* @param fromPosition The previous position of the item before the move.
* @param toPosition The new position of the item.
*/
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
/**
* Called when {@code count} number of items are updated at the given position.
* 當position位置的item內(nèi)容發(fā)生改變時調(diào)用
* @param position The position of the item which has been updated.
* @param count The number of items which has changed.
*/
@Override
public void onChanged(int position, int count, Object payload) {
mAdapter.notifyItemRangeChanged(position, count, payload);
}
}
(2)public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback)
/**
* Dispatches update operations to the given Callback.
將更新操作分派給指定的callback
* <p>
這些更新是原子性的,例如:第一個的更新會影響后面的更新
* These updates are atomic such that the first update call affects every update call that
* comes after it (the same as RecyclerView).
*
* @param updateCallback The callback to receive the update operations.
* @see #dispatchUpdatesTo(RecyclerView.Adapter)
*/
public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {
}
在該方法中計算出 item 的增刪改移動,然后將更新分配給指定的 callback,調(diào)用 AdapterListUpdateCallback 中對應的4個方法這個4個方法又最終會調(diào)用到onBindViewHolder()
中。
2. DiffUtil 的高級用法——整個數(shù)據(jù)源發(fā)生改變時的部分綁定
AdapterListUpdateCallback 類中的onItemRangeChanged
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
// fallback to onItemRangeChanged(positionStart, itemCount) if app
// does not override this method.,如果使用者沒有重寫該方法時,默認調(diào)用不帶payload的2個參數(shù)方法
onItemRangeChanged(positionStart, itemCount);
}
onBindViewHolder()
/**
* Called by RecyclerView to display the data at the specified position. This method
* should update the contents of the {@link ViewHolder#itemView} to reflect the item at
* the given position.
* <p>
* Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
* again if the position of the item changes in the data set unless the item itself is
* invalidated or the new position cannot be determined. For this reason, you should only
* use the <code>position</code> parameter while acquiring the related data item inside
* this method and should not keep a copy of it. If you need the position of an item later
* on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
* have the updated adapter position.
* <p>
* Partial bind vs full bind:
* <p>
* The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
* {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty,
* the ViewHolder is currently bound to old data and Adapter may run an efficient partial
* update using the payload info. If the payload is empty, Adapter must run a full bind.
* Adapter should not assume that the payload passed in notify methods will be received by
* onBindViewHolder(). For example when the view is not attached to the screen, the
* payload in notifyItemChange() will be simply dropped.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
* @param payloads A non-null list of merged payloads. Can be empty list if requires full
* update.
*/
public void onBindViewHolder(@NonNull VH holder, int position,
@NonNull List < Object > payloads) {
onBindViewHolder(holder, position);
}
Android】RecyclerView的好伴侶:詳解DiffUtil
【Android】 RecyclerView、ListView實現(xiàn)單選列表的優(yōu)雅之路.