DiffUtil這個控制元件工具出來也有一年多時間了,之前在專案中使用不是使用深拷貝集合,就是使用Serializable序列化反序列化資料,然後進行新舊資料對比重新整理,這樣的操作即很麻煩,也很不優雅,然後之前看了谷歌元件化架構中的Paging Library的原始碼,發現谷歌對於DiffUtil的封裝用法挺不錯的,所以今天我就我的思路來封裝一下DuffUtil。
首先我先考慮adapter重新整理的問題,每次資料更改,就要自己手動去呼叫adapter的notify方法,想要動畫效果的話,還得自己算下重新整理的位置,移除或者新增的重新整理方法,想到如果直接在資料更新完成後就自動呼叫adapter的重新整理方法就好了,既然這樣,那就先從集合資料入手,既然要重新整理資料,那就在對應集合的增刪改查中新增對應的重新整理程式碼吧,這裡我用介面把重新整理的需求回撥出去,整體程式碼如下:
/**
* #PagedList
*/
class PagedList<T> extends ArrayList<T> {
private Callback mCallback;
public void setCallback(Callback callback) {
mCallback = callback;
}
@Override
public boolean add(T t) {
boolean add = super.add(t);
if (add && mCallback != null) {
mCallback.onInserted(size() - 1, 1);
}
return add;
}
@Override
public void add(int index, T element) {
super.add(index, element);
if (mCallback != null) {
mCallback.onInserted(index, 1);
}
}
@Override
public boolean addAll(Collection<? extends T> c) {
boolean b = super.addAll(c);
if (b && mCallback != null) {
mCallback.onInserted(size() - c.size(), c.size());
}
return b;
}
@Override
public boolean addAll(int index, Collection<? extends T> c) {
boolean b = super.addAll(index, c);
if (b && mCallback != null) {
mCallback.onInserted(index, c.size());
}
return b;
}
@Override
public T remove(int index) {
T remove = super.remove(index);
if (remove != null && mCallback != null) {
mCallback.onRemoved(index, 1);
}
return remove;
}
@Override
public boolean removeAll(Collection<?> c) {
boolean b = super.removeAll(c);
if (b && mCallback != null) {
mCallback.onRemoved(size(), c.size());
}
return b;
}
@Override
protected void removeRange(int fromIndex, int toIndex) {
super.removeRange(fromIndex, toIndex);
if (mCallback != null) {
mCallback.onRemoved(fromIndex, toIndex - fromIndex);
}
}
@Override
public boolean remove(Object o) {
boolean remove = super.remove(o);
if (remove && mCallback != null) {
mCallback.onRemoved(indexOf(o), 1);
}
return remove;
}
@Override
public void clear() {
int size = size();
super.clear();
if (mCallback != null) {
mCallback.onRemoved(0, size);
}
}
/**
* 用於重新整理資料的回撥.
*/
public interface Callback {
/**
* 插入資料.
*/
public void onInserted(int position, int count);
/**
* 移除資料.
*/
@SuppressWarnings("unused")
public void onRemoved(int position, int count);
}
}複製程式碼
程式碼很簡單,就是在add,move這些會引起資料變化的方法裡,插入回撥方法的執行。然後重要的就是這個回撥的實現了。
現在開始就有個問題了,因為一般我們的網路資料經過gson轉換後的一般都是ArrayList而不是我們現在這個PagedList,這個我們就得想到如何把ArrayList轉換為PagedList,而且要做到在開發頁面不接觸到PagedList,這時候就要提供一個方法轉換了,我想的方法是這個:
public static <T> List<T> getConvertList(List<T> listData) {
PagedList<T> list = new PagedList<>();
list.addAll(listData);
return list;
}複製程式碼
這樣就可以將ArrayList轉為PagedList,而且也不會在開發頁面中出現PagedList了,這個方法我就先放到基類的adapter中了,
接下來就要考慮讓使用者去實現的DiffUtil.Callback方法了,不過由於自己強迫症,我還是不想讓開發頁面接觸到DiffUtil.Callback方法,所以就只能自己模仿寫個回撥的類了。程式碼如下
public abstract class DiffCallBack<T> {
public abstract boolean areItemsTheSame(T oldItem, T newItem);
public abstract boolean areContentsTheSame(T oldItem, T newItemn);
@Nullable
public Object getChangePayload(T oldItem, T newItem) {
return null;
}
}複製程式碼
就三個方法,和DiffUtil.Callback方法一樣,只不過引數從position變為具體的實體類了。
現在貌似給開發頁面需要的東西已經齊了,那就可以開始構造基類的adapter了。這裡想著如果基類adapter中還處理diffUtil的東西,感覺耦合過重,所以我們分出一個類專門處理diffUtil的一系列事件。
開始構造處理diff的類,DiiffAdapterHelper:
在DiiffAdapterHelper我們需要實現兩個功能,一就是PagedList中的回撥方法,我們要實現出來,二就是不同的兩個集合資料我們要呼叫DiffUtil的計算功能,這裡也涉及到執行緒,自然而然可以使用執行緒池了。
首先把PagedList的回撥實現:
/**
* 設定pagedList的回撥.
*/
private void setPagedCallback() {
PagedList list = (PagedList) mListData;
list.setCallback(new PagedList.Callback() {
@Override
public void onInserted(int position, int count) {
mBaseAdapter.notifyItemRangeInserted(position, count);
mBaseAdapter.notifyItemRangeChanged(position, count);
}
@Override
public void onRemoved(int position, int count) {
mBaseAdapter.notifyItemRangeRemoved(position, count);
mBaseAdapter.notifyItemRangeChanged(position, count);
}
});
}複製程式碼
把對應的集合傳進來,這樣就可以強轉成PagedList進行操作,當然也可能傳進來是ArrayList,這個處理待會再做。
然後再建立對應的DiffUtil.Callback,這裡我希望只有一個DiffUtil.Callback物件,而且新舊資料可以自己傳,這裡再構造一個DiffUtil.Callback:
public abstract class BaseCallBack<T> extends DiffUtil.Callback {
protected List<T> mOldData;
protected List<T> mNewData;
public BaseCallBack() {
}
public BaseCallBack(@NonNull List<T> oldData, @NonNull List<T> newData) {
mOldData = oldData;
mNewData = newData;
}
public void setOldData(List<T> oldData) {
mOldData = oldData;
}
public void setNewData(List<T> newData) {
mNewData = newData;
}
@Override
public int getOldListSize() {
return mOldData.size();
}
@Override
public int getNewListSize() {
return mNewData.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsTheSame(mOldData.get(oldItemPosition), mNewData.get(newItemPosition));
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return areContentsTheSame(mOldData.get(oldItemPosition), mNewData.get(newItemPosition));
}
public abstract boolean areItemsTheSame(T oldItem, T newItem);
public abstract boolean areContentsTheSame(T oldItem, T newItem);
}複製程式碼
這個本來是要給開發頁面去實現抽象方法的,但是總覺得要把diffUtil隱藏起來,所以這個就給這裡底層用了,然後就可以建立對應的Callback了。
/**
* 建立真正的diffUtilCallback.
*/
private void createNewDiff(final DiffCallBack<T> diffCallback) {
mDiffCallback = new BaseCallBack<T>() {
@Override
public boolean areItemsTheSame(T oldItem, T newItem) {
return diffCallback.areItemsTheSame(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(T oldItem, T newItem) {
return diffCallback.areContentsTheSame(oldItem, newItem);
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return diffCallback.getChangePayload(mOldData.get(oldItemPosition), mNewData.get(newItemPosition));
}
};
}複製程式碼
只要拿到使用者傳的的DiffCallBack,就可以組裝出真正的diff物件了,
還有計算資料,那肯定得線上程中操作,出於規範,這裡用了執行緒池。還有個handler切換到主執行緒重新整理。
/**
* 執行緒池(固定2個).
*/
private ExecutorService mExecutorService = Executors.newFixedThreadPool(2);
/**
* 切換主執行緒用.
*/
private Handler mHandler = new Handler();
mExecutorService.submit(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(mDiffCallback, true);
mHandler.post(new Runnable() {
@Override
public void run() {
diffResult.dispatchUpdatesTo(mBaseAdapter);
}
});
}
});複製程式碼
基本問題全部解決了,再來看完整的:
public class DiffAdapterHelper<T> {
/**
* 執行緒池(固定2個).
*/
private ExecutorService mExecutorService = Executors.newFixedThreadPool(2);
/**
* diff計算.
*/
private BaseCallBack<T> mDiffCallback;
/**
* 對應的adapter.
*/
private BaseAdapter<T> mBaseAdapter;
/**
* 資料.
*/
private List<T> mListData;
/**
* 切換主執行緒用.
*/
private Handler mHandler = new Handler();
public DiffAdapterHelper(List<T> listData, BaseAdapter<T> baseAdapter, DiffCallBack<T> diffCallback) {
mListData = listData;
mBaseAdapter = baseAdapter;
createNewDiff(diffCallback);
setPagedCallback();
}
/**
* 設定pagedList的回撥.
*/
private void setPagedCallback() {
PagedList list = (PagedList) mListData;
list.setCallback(new PagedList.Callback() {
@Override
public void onInserted(int position, int count) {
mBaseAdapter.notifyItemRangeInserted(position, count);
mBaseAdapter.notifyItemRangeChanged(position, count);
}
@Override
public void onRemoved(int position, int count) {
mBaseAdapter.notifyItemRangeRemoved(position, count);
mBaseAdapter.notifyItemRangeChanged(position, count);
}
});
}
/**
* 建立真正的diffUtilCallback.
*/
private void createNewDiff(final DiffCallBack<T> diffCallback) {
mDiffCallback = new BaseCallBack<T>() {
@Override
public boolean areItemsTheSame(T oldItem, T newItem) {
return diffCallback.areItemsTheSame(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(T oldItem, T newItem) {
return diffCallback.areContentsTheSame(oldItem, newItem);
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return diffCallback.getChangePayload(mOldData.get(oldItemPosition), mNewData.get(newItemPosition));
}
};
}
/**
* 設定新值.
* <p>
* 通過新舊資料對比計算,進入執行緒池計算
*
* @param listData 列表資料
*/
void setListData(final List<T> listData) {
List<T> oldData = mListData;
this.mListData = listData;
setPagedCallback();
mDiffCallback.setOldData(oldData);
mDiffCallback.setNewData(mListData);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(mDiffCallback, true);
mHandler.post(new Runnable() {
@Override
public void run() {
//計算結束才開始賦值並重新整理
mBaseAdapter.setRealListData(listData);
diffResult.dispatchUpdatesTo(mBaseAdapter);
}
});
}
});
}複製程式碼
注意一點,我這裡的思路是每次有新的資料,而且之前的資料不需要時候,就直接賦值新的集合,例如下拉重新整理那樣的場景,這樣就不用自己去深拷貝原先集合,從而可以用diffUtil進行計算。
接下倆就是構造基類的adapter了。
public abstract class BaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
protected Context mContext;
/**
* 集合.
*/
protected List<T> mListData;
/**
* 佈局.
*/
protected int layoutId;
/**
* 輔助計算.
*/
protected DiffAdapterHelper<T> mAdapterHelper;
public BaseAdapter(@NonNull List<T> listData, @LayoutRes int layoutId, DiffCallBack<T> callBack) {
this.mListData = listData;
this.layoutId = layoutId;
if (mListData instanceof PagedList) {
mAdapterHelper = new DiffAdapterHelper<>(mListData, this, callBack);
}
}
public static <T> List<T> getConvertList(List<T> listData) {
PagedList<T> list = new PagedList<>();
list.addAll(listData);
return list;
}
public final void setListData(List<T> listData) {
if (mAdapterHelper != null) {
mAdapterHelper.setListData(listData);
} else {
setRealListData(listData);
}
}
protected final void setRealListData(List<T> listData) {
mListData = listData;
}
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
mContext = parent.getContext();
return new BaseViewHolder(LayoutInflater.from(mContext)
.inflate(layoutId, parent, false));
}
@Override
public int getItemCount() {
return mListData.size();
}
}複製程式碼
這裡的adapter就很簡單了,只是負責判斷是否PagedList,不然就不建立DiffAdapterHelper,其他的好像沒啥了
到這裡就封裝結束了,來開始寫頁面了。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.yzl.diff.difftest.MainActivity">
<Button
android:id="@+id/bt_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="增加資料"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/bt_sub"/>
<Button
android:id="@+id/bt_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="減少資料"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintLeft_toRightOf="@+id/bt_add"
app:layout_constraintRight_toLeftOf="@+id/bt_change"/>
<Button
android:id="@+id/bt_change"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="切換資料來源"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintLeft_toRightOf="@+id/bt_sub"
app:layout_constraintRight_toRightOf="parent"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layoutManager="LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/bt_add"/>
</android.support.constraint.ConstraintLayout>
複製程式碼
public class MainActivity extends AppCompatActivity {
private Button mBtAdd;
private Button mBtSub;
private Button mBtChange;
private RecyclerView mRv;
private BaseAdapter<MockData> mAdapter;
private List<MockData> mMockDataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initList();
initListener();
}
private void initView() {
mBtAdd = (Button) findViewById(R.id.bt_add);
mBtSub = (Button) findViewById(R.id.bt_sub);
mBtChange = (Button) findViewById(R.id.bt_change);
mRv = (RecyclerView) findViewById(R.id.rv);
}
private void initList() {
mMockDataList = new ArrayList<>();
mMockDataList.add(new MockData(1, "一1"));
mMockDataList.add(new MockData(2, "一2"));
mMockDataList.add(new MockData(3, "一3"));
mMockDataList.add(new MockData(4, "一4"));
mMockDataList.add(new MockData(5, "一5"));
mMockDataList.add(new MockData(6, "一6"));
mMockDataList.add(new MockData(7, "一7"));
mMockDataList.add(new MockData(8, "一8"));
mMockDataList.add(new MockData(9, "一9"));
mMockDataList.add(new MockData(10, "一10"));
mMockDataList = BaseAdapter.getConvertList(mMockDataList);
mAdapter = new MainAdapter(mMockDataList, R.layout.item_main, new DiffCallBack<MockData>() {
@Override
public boolean areItemsTheSame(MockData oldItem, MockData newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(MockData oldItem, MockData newItem) {
return oldItem.getName().equals(newItem.getName());
}
});
mRv.setAdapter(mAdapter);
}
private void initListener() {
mBtAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Random random = new Random();
mMockDataList.add(random.nextInt(mMockDataList.size()), new MockData(mMockDataList.size() + 1, "一2"));
}
});
mBtSub.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMockDataList.remove(mMockDataList.size() - 1);
}
});
mBtChange.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
List<MockData> data = new ArrayList<>();
data.add(new MockData(1, "一8"));
data.add(new MockData(2, "一2"));
data.add(new MockData(3, "一3"));
data.add(new MockData(8, "一1"));
data.add(new MockData(9, "一9"));
data.add(new MockData(10, "一10"));
mMockDataList = BaseAdapter.getConvertList(data);
mAdapter.setListData(mMockDataList);
}
});
}
}複製程式碼
執行效果如圖
大概效果和程式碼就是這樣了,本文只是簡單提供下我的diffUtil的封裝思路,希望會對你有點幫助,如果你理解了這個思路,你可以試著封裝一下,可能會想到更好的思路。
順便安利一下我之前寫的 PagingLibrary原始碼淺析:juejin.im/post/5a08fe…
谷歌的 這個庫確實很贊,你可以去看下原始碼感受一下。
本文demo地址:github.com/a1048823898…