DiffUtil之我的封裝思路

順其自然55發表於2019-03-01

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之我的封裝思路

大概效果和程式碼就是這樣了,本文只是簡單提供下我的diffUtil的封裝思路,希望會對你有點幫助,如果你理解了這個思路,你可以試著封裝一下,可能會想到更好的思路。

順便安利一下我之前寫的 PagingLibrary原始碼淺析:juejin.im/post/5a08fe…

谷歌的 這個庫確實很贊,你可以去看下原始碼感受一下。

本文demo地址:github.com/a1048823898…


相關文章