[Android] DiffUtil在RecyclerView中的使用詳解

概述

DiffUtil是recyclerview support library v7 24.2.0版本中新增的類,根據Google官方文檔的介紹,DiffUtil的做用是比較兩個數據列表並能計算出一系列將舊數據錶轉換成新數據表的操做。這個概念比較抽象,換一種方式理解,DiffUtil是一個工具類,當你的RecyclerView須要更新數據時,將新舊數據集傳給它,它就能快速告知adapter有哪些數據須要更新。php

那麼相比直接調用adapter.notifyDataSetChange()方法,使用DiffUtil有什麼優點呢?它能在收到數據集後,提升UI更新的效率,並且你也不須要本身對新老數據集進行比較了。java

顧名思義,凡是數據集的比較DiffUtil都能作,因此用處並不止於更新RecyclerView。DiffUtil也提供了回調讓你能夠進行其餘操做。本文只介紹使用DiffUtil更新RecyclerView。android

DiffUtil簡介

在使用DiffUtil前咱們先簡單看看DiffUtil的特性。DiffUtil使用Eugene W. Myers的Difference算法來計算出將一個數據集轉換爲另外一個的最小更新量,也就是用最簡單的方式將一個數據集轉換爲另外一個。除此以外,DiffUtil還能夠識別一項數據在數據集中的移動。Eugene的算法對控件進行了優化,在查找兩個數據集間最少加減操做時的空間複雜度爲O(N),時間複雜度爲O(N+D^2)。而若是添加了對數據條目移動的識別,複雜度就會提升到O(N^2)因此若是數據集中數據不存在移位狀況,你能夠關閉移動識別功能來提升性能。當數據集較大時,你應該在後臺線程計算數據集的更新。git

如何使用

DiffUtil類

  • DiffUtil.Callback:這是最核心的類,你能夠將它理解成比較新老數據集時的規則。github

  • DiffUtil:經過靜態方法DiffUtil.calculateDiff(DiffUtil.Callback)來計算數據集的更新。算法

  • DiffResult:是DiffUtil的計算結果對象,經過DiffResult.dispatchUpdatesTo(RecyclerView.Adapter)來進行更新。app

代碼模式爲異步

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
mAdapter.setDatas(newDatas);
diffResult.dispatchUpdatesTo(mAdapter);

dispatchUpdatesTo()方法它會自動計算新老數據集的差別,並根據差別狀況,自動調用如下四個方法ide

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

DiffUtil.Callback抽象類

public abstract static class Callback {
        /**
         * 返回舊數據集的大小
         *
         * @return The size of the old list.
         */
        public abstract int getOldListSize();

        /**
         * 返回新數據集的大小
         *
         * @return The size of the new list.
         */
        public abstract int getNewListSize();

        /**
         * 比較兩個Item對象是不是同一個對象
         *
         * @param oldItemPosition The position of the item in the old list
         * @param newItemPosition The position of the item in the new list
         * @return True if the two items represent the same object or false if they are different.
         */
        public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

        /**
         * 比較兩個Item對象的內容是否相同
         * Called by the DiffUtil when it wants to check whether two items have the same data.
         * DiffUtil uses this information to detect if the contents of an item has changed.
         * <p>
         * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
         * so that you can change its behavior depending on your UI.
         * For example, if you are using DiffUtil with a
         * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
         * return whether the items' visual representations are the same.
         * <p>
         * This method is called only if {@link #areItemsTheSame(int, int)} returns
         * {@code true} for these items.
         *
         * @param oldItemPosition The position of the item in the old list
         * @param newItemPosition The position of the item in the new list which replaces the
         *                        oldItem
         * @return True if the contents of the items are the same or false if they are different.
         */
        public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);

        /**
         * areItemsTheSame()返回true而areContentsTheSame()返回false時調用,也就是說兩個對象表明的數據是一條,可是內容更新了。
         * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
         * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
         * calls this method to get a payload about the change.
         * <p>
         * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the
         * particular field that changed in the item and your
         * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
         * information to run the correct animation.
         * <p>
         * Default implementation returns {@code null}.
         *
         * @param oldItemPosition The position of the item in the old list
         * @param newItemPosition The position of the item in the new list
         *
         * @return A payload object that represents the change between the two items.
         */
        @Nullable
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            return null;
        }
    }

DiffUtil步驟

  1. 自定義類繼承DiffUtil.Callback,經過重寫特定方法給出數據比較邏輯。工具

  2. 調用DiffUtil.calculateDiff(DiffUtil.Callback callback,boolean detectMove)來計算更新,獲得DiffResult對象。第二個參數可省,意爲是否探測數據的移動,是否關閉須要根據數據集狀況來權衡。當數據集很大時,此操做可能耗時較長,須要異步計算。

  3. 在UI線程中調用DiffResult.dispatchUpdatesTo(RecyclerView.Adapter),然後Adapter的onBindViewHolder(RecyclerView.ViewHolder holder, int position, Listpayloads)。注意這個方法比必須覆蓋的onBindViewHolder(RecyclerView.ViewHolder holder, int position)方法多一個參數payloads,而裏面存儲了數據的更新。

示例

初始化RecyclerView

  • 新建一個Bean爲Item:

package com.michael.materialdesign.bean;

/**
 * Created by liuguoquan on 2016/10/18.
 */

public class Item {

  public int id = 0;
  public String name;

  public Item(int id, String name) {
    this.id = id;
    this.name = name;
  }

  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;
  }
}
  • 新建Adapter

public class DiffUtilAdapter extends RecyclerView.Adapter<DiffUtilAdapter.DiffItemHolder> {

  private Context mContext;
  private List<Item> mDatas;

  public DiffUtilAdapter(Context context, List<Item> datas) {
    this.mContext = context;
    this.mDatas = datas;
  }

  public void setDatas(List<Item> mDatas) {
    this.mDatas = mDatas;
  }

  @Override public DiffItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(mContext).inflate(R.layout.item_diff_util, parent, false);
    return new DiffItemHolder(view);
  }

  @Override public void onBindViewHolder(DiffItemHolder holder, int position) {
    Item info = mDatas.get(position);
    holder.mInfo.setText(info.getName());
    Log.d("lgq","onBindViewHolder");
  }

    //payloads就是DiffUtil.Callback中的getChangePayload方法返回的數據集
  @Override
  public void onBindViewHolder(DiffItemHolder holder, int position, List<Object> payloads) {
    
    if (payloads.isEmpty()) {
      onBindViewHolder(holder,position);
    } else {
    //更新item
      Bundle bundle = (Bundle) payloads.get(0);
      for(String key : bundle.keySet()) {
        switch (key) {
          case "name":
            holder.mInfo.setText((CharSequence) bundle.get(key));
            break;
        }
      }
    }
  }

  @Override public int getItemCount() {
    return mDatas != null ? mDatas.size() : 0;
  }

  static class DiffItemHolder extends RecyclerView.ViewHolder {

    @BindView(R.id.info) TextView mInfo;

    public DiffItemHolder(View itemView) {
      super(itemView);
      ButterKnife.bind(this, itemView);
    }
  }
}
  • 初始化ReyclerView

private void initView() {

    for(int i = 0; i < 20;i++) {
      Item item = new Item(i,"liu"+i);
      mDatas.add(item);
    }

    mAdapter = new DiffUtilAdapter(this,mDatas);
    mList.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));
    mList.setItemAnimator(new DefaultItemAnimator());
    mList.setAdapter(mAdapter);

  }

初始化RecyclerView後效果爲:

實現DiffUtil.Callback

新建類繼承DiffUtil.Callback

private class DiffCallback extends DiffUtil.Callback {

    private List<Item> mOldDatas;
    private List<Item> mNewDatas;

    //傳入舊數據和新數據的集合
    public DiffCallback(List<Item> oldDatas,List<Item> newDatas) {
      this.mOldDatas = oldDatas;
      this.mNewDatas = newDatas;
    }

    @Override public int getOldListSize() {
      return mOldDatas != null ? mOldDatas.size() : 0;
    }

    @Override public int getNewListSize() {
      return mNewDatas != null ? mNewDatas.size() : 0;
    }

    /**
     * 被DiffUtil調用,用來判斷 兩個對象是不是相同的Item。
     * 例如,若是你的Item有惟一的id字段,這個方法就 判斷id是否相等。
     * 本例判斷id字段是否一致
     */
    @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
      boolean is = mOldDatas.get(oldItemPosition).id == mNewDatas.get(newItemPosition).id;
      Log.d("lgq","areItemsTheSame " +oldItemPosition + " " + newItemPosition + " " + is);
      return is;
    }

    /*
     * 被DiffUtil調用,用來檢查 兩個item是否含有相同的數據
     * 這個方法僅僅在areItemsTheSame()返回true時,才調用。
     * @param oldItemPosition The position of the item in the old list
     * @param newItemPosition The position of the item in the new list which replaces the
     *                        oldItem
     * @return True if the contents of the items are the same or false if they are different.
     */
    @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
      String oldName = mOldDatas.get(oldItemPosition).getName();
      String newName = mNewDatas.get(newItemPosition).getName();
      Log.d("lgq","areContentsTheSame"
          + " " +oldName + " " + newName);
      if (!oldName.equals(newName)) {
        Log.d("lgq","false");
        return false;
      }
      return true;
    }

    /**
     * areItemsTheSame()返回true而areContentsTheSame()返回false,也就是說兩個對象表明的數據是一條,可是內容更新了。
     * @param oldItemPosition
     * @param newItemPosition
     * @return
     */
    @Nullable @Override public Object getChangePayload(int oldItemPosition, int newItemPosition) {
      String oldItem = mOldDatas.get(oldItemPosition).getName();
      String newItem = mNewDatas.get(newItemPosition).getName();
      Bundle bundle = new Bundle();
      if (!oldItem.equals(newItem)) {
          bundle.putString("name",newItem);
      }

      if (bundle.size() == 0) {
        return null;
      }
      Log.d("lgq","getChangePayload");
      return bundle;
    }
  }

使用DiffUtil

下面經過兩種不一樣的改變RecyclerView條目來介紹DiffUtil的使用。

  • 增長或刪除條目

這種狀況下,數據集的大小改變,反映在RecyclerView的效果就是增長或者刪除條目

private void add() {

    mNewDatas.clear();
    mNewDatas.addAll(mDatas);
    mNewDatas.add(new Item(89,"xiao"));
    mNewDatas.add(new Item(90,"xia"));
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(mDatas,mNewDatas),true);
    mAdapter.setDatas(mNewDatas);
    diffResult.dispatchUpdatesTo(mAdapter);
    mDatas.clear();
    mDatas.addAll(mNewDatas);
  }

增長條目後的RecyclerView的效果爲:

  • 更新具體的條目

這種狀況下數據集大小不改變,改變數據集中條目的內容,反映在RecyclerView的效果就是更新具體的條目,這回調用Callback中的getChangePayload方法,而Adapter必需要實現public void onBindViewHolder(DiffItemHolder holder, int position, List<Object> payloads)方法。

private void refresh() {
    mNewDatas.clear();
    mNewDatas.addAll(mDatas);
    //改變第三個位置的對象
    Item item = new Item(3,"zhang");
    mNewDatas.remove(3);
    mNewDatas.add(3,item);
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(mDatas,mNewDatas),true);
    //將新數據給Adapter
    mAdapter.setDatas(mNewDatas);
    diffResult.dispatchUpdatesTo(mAdapter);
    mDatas.clear();
    mDatas.addAll(mNewDatas);
  }

更新條目後的RecyclerView效果爲:

由圖可知,第四個位置的條目顯示變爲zhang。

結語

DiffUtil可用於高效進行RecyclerView的數據更新,但DiffUtil自己的做用是計算數據集的最小更新。DiffUtil有強大的算法支撐,能夠利用DiffUtil完成許多其餘功能。

示例代碼

在RecyclerView目錄下

參考文章:

使用DiffUtil高效更新RecyclerView
詳解7.0帶來的新工具類:DiffUtil

相關文章
相關標籤/搜索