前言

Android开发几乎每天都会和各种列表打交道,目前我们主要用的就是RecyclerView,古老的ListView相信已经没有多少项目在用了;网络上对其使用分析及相关技巧的文章比较多,但是大多数给出了解决方案,却没有交代清楚使用场景,以至于这些文章只是变成了我们的知识储备,但却没有变成我们解决问题的能力。

关于RecyclerView相关的知识点很多,先来学习一个能够提升列表更新性能的小工具DiffUtil,它在源码中的位置如图所示

picture

DiffUtil的作用

它是一个检查两个列表数据集之间变化的回调,默认在RecyclerView.Adapter中已经有了相关的使用,如那些以notifyItemXXX开头的方法,都是比notifyDataSetChanged更加高效的更新列表数据的方法;

DiffUtil与notifyDataSetChanged的对比

  • notifyDataSetChanged是一个很重的操作,因为无论何时调用它都会更新每一个Item,而大多数情况只有列表中的几条数据需要更新;

  • DiffUtil则不同,它只会更新两个列表数据集之间有变化的Item;

一言以蔽之:凡是之前用notifyDataSetChanged()的地方都可以使用DiffUtil优化更新性能

一个示例应用场景

先看结果 效果

项目结构如图所示 Project Structure 布局文件不就放了,很简单

数据类Model.java

public class Model implements Comparable, Cloneable {

    public String name;
    public int id, price;

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

    /**
    *后续结合DiffUtil实现内容的比较
    *可以在这里直接实现Comparable接口,让数据类自己管理内容变化的逻辑
    */
    @Override
    public int compareTo(Object o) {
        Model compare = (Model) o;

        if (compare.id == this.id && compare.name.equals(this.name) && compare.price == (this.price)) {
            return 0;
        }
        return 1;
    }

    @Override
    public Model clone() {

        Model clone;
        try {
            clone = (Model) super.clone();

        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e); //should not happen
        }

        return clone;
    }

}

界面MainActivity.java

public class MainActivity extends AppCompatActivity {


    RecyclerView recyclerView;
    RecyclerViewAdapter recyclerViewAdapter;
    FloatingActionButton fabAddList, fabChangeList;

    private ArrayList<Model> modelArrayList = new ArrayList<>();

    public int i = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        recyclerView = findViewById(R.id.recyclerView);
        fabAddList = findViewById(R.id.fabAddList);
        fabChangeList = findViewById(R.id.fabChangeList);

        dummyData();
        recyclerViewAdapter = new RecyclerViewAdapter(modelArrayList);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.setAdapter(recyclerViewAdapter);

        fabAddList.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                addMoreCoinsToTheList();
            }
        });

        fabChangeList.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                changePricesInTheList();
            }
        });
        
    }

    private void dummyData() {

        modelArrayList.add(new Model(i++, "Bitcoin", 8000));
        modelArrayList.add(new Model(i++, "Ethereum", 600));
        modelArrayList.add(new Model(i++, "Litecoin", 250));
        modelArrayList.add(new Model(i++, "Bitcoin Cash", 1000));
    }

    private void addMoreCoinsToTheList() {
        ArrayList<Model> models = new ArrayList<>();

        for (Model model : modelArrayList) {
            models.add(model.clone());
        }
        models.add(new Model(i++, "Tron", 1));
        models.add(new Model(i++, "Ripple", 5));
        models.add(new Model(i++, "NEO", 100));
        models.add(new Model(i++, "OMG", 20));

        recyclerViewAdapter.setData(models);
    }

    private void changePricesInTheList() {

        ArrayList<Model> models = new ArrayList<>();
        
        for (Model model : modelArrayList) {
            models.add(model.clone());
        }

        for (Model model : models) {
            if (model.price < 900)
                model.price = 900;
        }
        recyclerViewAdapter.setData(models);
    }

}

RecyclerViewAdapter.java

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.CryptoViewHolder> {

    private ArrayList<Model> data;

    public class CryptoViewHolder extends RecyclerView.ViewHolder {

        private TextView mName, mPrice;

        public CryptoViewHolder(View itemView) {
            super(itemView);
            mName = itemView.findViewById(R.id.txtName);
            mPrice = itemView.findViewById(R.id.txtPrice);
        }
    }

    public RecyclerViewAdapter(ArrayList<Model> data) {
        this.data = data;
    }

    @Override
    public CryptoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview_item_layout, parent, false);
        return new CryptoViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(CryptoViewHolder holder, int position) {
        holder.mName.setText(data.get(position).name);
        holder.mPrice.setText(data.get(position).price + " USD");
    }
     /**
     *
     *注意第三个参数的变化的数据部分
     */
    @Override
    public void onBindViewHolder(CryptoViewHolder holder, int position, List<Object> payloads) {

        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            Bundle o = (Bundle) payloads.get(0);
            for (String key : o.keySet()) {
                if (key.equals("price")) {
                    holder.mName.setText(data.get(position).name);
                    holder.mPrice.setText(data.get(position).price + " USD");
                    holder.mPrice.setTextColor(Color.GREEN);
                }
            }
        }
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    public ArrayList<Model> getData() {
        return data;
    }
     /**
     *
     *扩展的方法,DiffUtil的使用
     */
    public void setData(ArrayList<Model> newData) {

        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffUtilCallBack(newData, data));
        diffResult.dispatchUpdatesTo(this);
        data.clear();
        this.data.addAll(newData);
    }
}

重点来了MyDiffUtilCallback.java

public class MyDiffUtilCallBack extends DiffUtil.Callback {
    ArrayList<Model> newList;
    ArrayList<Model> oldList;

    public MyDiffUtilCallBack(ArrayList<Model> newList, ArrayList<Model> oldList) {
        this.newList = newList;
        this.oldList = oldList;
    }
     /**
     *必须要实现的方法一:
     *这里要返回旧数据集的size
     */
    @Override
    public int getOldListSize() {
        return oldList != null ? oldList.size() : 0;
    }
    /**
     *必须要实现的方法二:
     *这里要返回新数据集的size
     */
    @Override
    public int getNewListSize() {
        return newList != null ? newList.size() : 0;
    }
     /**
     *必须要实现的方法三:
     *判断两个Item的数据是否相同,一般可以根据业务中不变的熟悉判断
     *如:文字Id,商品Id,用户Id等Ids
     *@return 重点: true 表示该position是同一条数据(属性可能发生变化,还要二次判断),DiffUtil会把数据传给areContentsTheSame()进行处理;false时 完全不是同一条数据
     */
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return newList.get(newItemPosition).id == oldList.get(oldItemPosition).id;
    }

     /**
     *必须要实现的方法四:
     *判断两个Item的数据的内容是否相同,一般可以根据具体业务判断
     *这里直接让数据类实现了`Comparable`接口管理内容的变化的判断
     *@return 重点: true 表示数据没有变化,交给DiffUtil管理;false 数据内容有变化
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        int result = newList.get(newItemPosition).compareTo(oldList.get(oldItemPosition));
        return result == 0;
    }
     /**
     *必须要实现的方法五:
     *当且仅当areItemsTheSame返回true,areContentsTheSame返回false时,该方法才调用
     *在这里,我们可以检测数据的属性是否发生了变化。然后我们可以使用 
     *Bundle传递更改的值。它将在我们的RecyclerView Adapter类的`onBindViewHolder(CryptoViewHolder holder, int position, List<Object> payloads)`中收到。
     */
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        
        Model newModel = newList.get(newItemPosition);
        Model oldModel = oldList.get(oldItemPosition);

        Bundle diff = new Bundle();

        if (newModel.price != (oldModel.price)) {
            diff.putInt("price", newModel.price);
        }
        if (diff.size() == 0) {
            return null;
        }
        return diff;
        //return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}