RecyclerView.Adapter:全能notify解决方案

在之前我们用 ListView 或者 GridView 的时候,通知适配器刷新是这样的:

1
adapter.notifyDataSetChanged();

但是当我们使用了更强大的 RecyclerView 之后,如果直接这样通知适配器刷新将不会显示动画效果。它会直接将所有的 item 重新绘制。

我们需要使用如下的方法来通知适配器刷新,这样 RecyclerView 才会显示对应的动画效果:

1
2
3
4
5
6
7
adapter.notifyItemInserted();
adapter.notifyItemChanged();
adapter.notifyItemMoved();
adapter.notifyItemRemoved();
adapter.notifyItemRangeChanged();
adapter.notifyItemRangeInserted();
adapter.notifyItemRangeRemoved();

在这次更新的 Support Library 24.2.0 中添加了一个新的工具类,可以用来方便快捷的处理 RecyclerView.Adapter 的通知刷新。

DiffUtil

DifUtil 就是这次引入的工具类,它会找出 Adapter 中每一个 Item 对应发生的变化,然后对每一个变化给予对应的刷新。

最重要的就是如下的两个重载方法

1
2
DifUtil.calculateDiff(Callback cb, boolean detectMoves);
DifUtil.calculateDiff(Callback cb);

其中DifUtil.calculateDiff(Callback cb);实际上就是DifUtil.calculateDiff(callback, true);所以我们着重研究第一个方法即可。

该方法会接收两个参数,其中第二个参数是一个 boolean 值,查看源码注释我们知道这个参数有如下作用:

True if DiffUtil should try to detect moved items, false otherwise.

如果 DiffUtil 尝试检测移动的项目就设为 true,否则设为 false。

这个参数实际上是指定是否需要项目移动的检测,如果设为 false ,那么一个项目移动了会先判定为 remove,再判定为 insert。

Callback是一个抽象类,它有四个方法需要实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract static class Callback {
/**
* 旧的数据源的大小
*/
public abstract int getOldListSize();

/**
* 新的数据源的大小
*/
public abstract int getNewListSize();

/**
* 该方法用于判断两个 Object 是否是相同的 Item,比如有唯一标识的时候应该比较唯一标识是否相等
*/
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

/**
* 当 areItemsTheSame 返回 true 时调用该方法,返回显示的 Item 的内容是否一致
*/
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
}

如上所述,我们四个需要实现的方法的作用都在注释中写出了。前两个方法都很好理解,需要重点说明的是后两个

  • areItemsTheSame:这个方法用来判断两个 Object 是否是相同的 Item,此处最好不要简单的用equals方法判断,我们可以根据 Object 的唯一标识或者自己指定一个规则来判断两个 Object 是否是展示的相同的 Item。
  • areContentsTheSame:该方法只有在areItemsTheSame返回true之后才会被调用,我们在重写该方法的时候,只需要判断两个 Object 显示的元素是否一致即可。如我们有两个 Object,它们可能拥有很多属性,但是其中只有两个属性需要被显示出来,那只要这两个属性一致我们这个方法就要返回true

使用 DiffUtils 通知刷新

下面我们写一个简单的例子来学习使用 DiffUtil

首先我们来一个 Item 对应的数据类:

1
2
3
4
5
6
7
8
9
public class Student {
public String id; // 学号是唯一的
public String name; // 名字可能重复

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

然后写一个 Adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

private final List<Student> datas;

public MyAdapter(List<Student> datas) {
this.datas = datas;
}

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

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData(datas.get(position));
}

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

class ViewHolder extends RecyclerView.ViewHolder {

public ViewHolder(View itemView) {
super(itemView);
}

public void setData(Student student) {
TextView textView = (TextView) this.itemView.findViewById(R.id.text);
textView.setText(student.name);
}
}
}

其对应的布局文件就是一个简单的 TextView:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
tools:text="content"/>

然后我们在 Activity 里使用它们并显示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
mRandom = new Random();
datas = new ArrayList<>();
for (int i = 0; i < 10; i++) {
datas.add(new Student(mRandom.nextInt(3000) + "", "Students: " + i));
}

mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new MyAdapter(datas);
mRecyclerView.setAdapter(mAdapter);
// ...
}
}

这样我们就获得了一个简单的展示学生数据的 RecyclerView 了。

然后我们对 Adapter 的数据源进行更改,并通知刷新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 创建一个原来的 List 的副本
final ArrayList<Student> oldTemp = new ArrayList<>(datas);
// 更改原数据源
datas.remove(mRandom.nextInt(mAdapter.getItemCount()));
for (int i = 0; i < mRandom.nextInt(3); i++) {
datas.add(mRandom.nextInt(mAdapter.getItemCount() - 1),
new Student(mRandom.nextInt(3000) + "", "Students: " + mRandom.nextDouble()));
}
// 实现 Callback
DiffUtil.Callback callback = new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldTemp.size();
}

@Override
public int getNewListSize() {
return datas.size();
}

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldTemp.get(oldItemPosition).id.equals(datas.get(newItemPosition).id);
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldTemp.get(oldItemPosition).name.equals(datas.get(newItemPosition).name);
}
};
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);
// 把结果应用到 adapter
diffResult.dispatchUpdatesTo(mAdapter);
}
});

效果如下:

adapter_diffutil

DiffUtil 的使用就是这样,根据 DiffUtil.Callback 计算出 Result,然后应用更新到 Adapter。

封装

有的人可能说了,这样其实并不好用啊,我们原来数据的改变就直接使用对应的方法就可以了,你这里每次还要写得这么麻烦。那么我们就使用 DiffUtil 和 Adapter 结合再进行一次封装吧。

我们抽取一个 BaseAdapter 出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public abstract class BaseAdapter<T, V extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<V>{

protected final List<T> temp; // 用于保存修改之前的数据源的副本
protected final List<T> datas; // 数据源

public BaseAdapter(List<T> datas) {
this.datas = datas;
temp = new ArrayList<>(datas);
}

protected abstract boolean areItemsTheSame(T oldItem, T newItem);

protected abstract boolean areContentsTheSame(T oldItem, T newItem);

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

public void notifyDiff() {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return temp.size();
}

@Override
public int getNewListSize() {
return datas.size();
}

// 判断是否是同一个 item
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return BaseAdapter.this.areItemsTheSame(temp.get(oldItemPosition), datas.get(newItemPosition));
}

// 如果是同一个 item 判断内容是否相同
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return BaseAdapter.this.areContentsTheSame(temp.get(oldItemPosition), datas.get(newItemPosition));
}
});
diffResult.dispatchUpdatesTo(this);
// 通知刷新了之后,要更新副本数据到最新
temp.clear();
temp.addAll(datas);
}
}

然后我们只需要令 Adapter 实现 BaseAdapter即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MyAdapter extends BaseAdapter<Student, MyAdapter.ViewHolder> {

public MyAdapter(List<Student> datas) {
super(datas);
}

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

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData(datas.get(position));
}

@Override
public boolean areItemsTheSame(Student oldItem, Student newItem) {
return oldItem.id.equals(newItem.id);
}

@Override
public boolean areContentsTheSame(Student oldItem, Student newItem) {
return oldItem.name.equals(newItem.name);
}

class ViewHolder extends RecyclerView.ViewHolder {

public ViewHolder(View itemView) {
super(itemView);
}

public void setData(Student student) {
TextView textView = (TextView) this.itemView.findViewById(R.id.text);
textView.setText(student.name);
}
}
}

之后我们如果数据源 List 中的数据有任何改动,我们只需要调用notifyDiff()就可以了:

1
2
3
4
5
6
7
8
9
10
11
mFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
datas.remove(mRandom.nextInt(mAdapter.getItemCount()));
for (int i = 0; i < mRandom.nextInt(3); i++) {
datas.add(mRandom.nextInt(mAdapter.getItemCount() - 1),
new Student(mRandom.nextInt(3000) + "", "Students: " + mRandom.nextDouble()));
}
mAdapter.notifyDiff();
}
});

总结

最新 Support 包中的 DiffUtil 类给我们带来了一个对 RecyclerView 的不同数据变化的统一处理方案,可以对所有数据变化之后的通知刷新简化,非常好用,强烈推荐使用。

参考

Android开发学习之路-DiffUtil使用教程

作者

Loshine

发布于

2016-08-25

更新于

2024-04-01

许可协议

评论