DiffUtil之我的封装思路

2,322 阅读8分钟

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.cn/post/684490…

谷歌的 这个库确实很赞,你可以去看下源码感受一下。

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