Android開発のDiffUtilの使用詳細
前に書いたら
ディffUtilはセットの変化を調べるためのツール類で、RecyclerViewと一緒に使うものです。もしまだRecyclerViewを知らないなら、いくつかの資料を読むことができます。ここでは紹介しません。
先送り効果図:
ボタンをクリックすると、このRecyclerViewで表示されているセットが変更され、一部の要素が追加されました(8.Jason)、また一部の要素が移動されました(3.Rose)、さらには修正されました(2.Fndroid)。
RecyclerViewは各Itemのアニメーションに対して異なる方法で更新されています。
notifyItem Inserted
notifyItem Changd
notifyItem Moved
notifyItem Removed
連続したいくつかのItemのリフレッシュに対しては、以下のように呼び出すことができる。
notifyItem Range Changd
notifyItem RangeInserted
notifyItem RangeRemoved
セットが変化すると、
DiffUtilの役割は、集合中のItemごとの変化を探し出し、その変化ごとに対応するリフレッシュを与えることです。
このDiffUtilはEugene Myersの差別アルゴリズムを使用しています。このアルゴリズム自体は元素の移動を確認することができません。つまり移動は先に削除し、また増加することしかできません。DiffUtilはアルゴリズムの結果の後にもう一度移動検査を行います。要素移動を検出しない場合、アルゴリズムの時間複雑度はO(N+D 2)であり、要素移動を検出すると複雑度はO(N 2)であると仮定する。したがって、集合自体が順序を整えていれば、移動の検知を行わずに効率を向上させることができる。
これはどうやって使うかを見に来ました。
まず、各Itemに対して、データはStudentオブジェクトである。
DiffUtilの使い方:
マウスを押すと、ArayListの内容を変更します。
9行目の呼び出し
10行目に新しいデータをAdapterに再設定します。
第11行目は、9行目で得られたDiffResultオブジェクトの
ここでは上記の
Itemの移動をチェックしない設定であれば、効果は以下の通りです。
次に、CallBackインターフェースの実装クラスがどのように定義されているかを見ます。
実際、このCallback抽象クラスにはもう一つの方法があります。この方法の役割は、この方法を通じて、全体の更新ではなく、このItemを部分的に更新することをAdapterに教えられます。
まず、このパスロードは何ですか?payloadはItemの変化を説明する対象です。つまり私達のItemにはどのような変化が発生しましたか?これらの変化は一つのpayloadにパッケージされています。だから私達は普通Bundelで働いてもいいです。
次に、
上の例の中学校番号と名前が異なるTextViewで表示されていると仮定します。学号に対応する名前を修正した場合、部分的に名前を書き換えてもいいです。
まずCallbackの中のこの方法を書き換えることです。
ここで注意します。RecyclerViewに大量のデータがロードされていると、アルゴリズムはすぐには完成しないかもしれません。ANRの問題に注意して、単独のスレッドを開いて計算することができます。
締め括りをつける
AndroidでDiffUtilを使って紹介しました。Androidの開発者たちの助けになりたいです。質問があれば、メッセージを書いて交流してください。
ディffUtilはセットの変化を調べるためのツール類で、RecyclerViewと一緒に使うものです。もしまだRecyclerViewを知らないなら、いくつかの資料を読むことができます。ここでは紹介しません。
先送り効果図:
ボタンをクリックすると、このRecyclerViewで表示されているセットが変更され、一部の要素が追加されました(8.Jason)、また一部の要素が移動されました(3.Rose)、さらには修正されました(2.Fndroid)。
RecyclerViewは各Itemのアニメーションに対して異なる方法で更新されています。
notifyItem Inserted
notifyItem Changd
notifyItem Moved
notifyItem Removed
連続したいくつかのItemのリフレッシュに対しては、以下のように呼び出すことができる。
notifyItem Range Changd
notifyItem RangeInserted
notifyItem RangeRemoved
セットが変化すると、
notifyDataSetChanged
メソッドを呼び出して、インターフェース全体のリフレッシュを行うだけで、セットの変化に応じて、各変化の要素にアニメーションを追加することはできません。だからここでディフティがこの問題を解決します。DiffUtilの役割は、集合中のItemごとの変化を探し出し、その変化ごとに対応するリフレッシュを与えることです。
このDiffUtilはEugene Myersの差別アルゴリズムを使用しています。このアルゴリズム自体は元素の移動を確認することができません。つまり移動は先に削除し、また増加することしかできません。DiffUtilはアルゴリズムの結果の後にもう一度移動検査を行います。要素移動を検出しない場合、アルゴリズムの時間複雑度はO(N+D 2)であり、要素移動を検出すると複雑度はO(N 2)であると仮定する。したがって、集合自体が順序を整えていれば、移動の検知を行わずに効率を向上させることができる。
これはどうやって使うかを見に来ました。
まず、各Itemに対して、データはStudentオブジェクトである。
class Student {
private String name;
private int num;
public Student(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
次にレイアウト(省略)とアダプターを定義します。
class MyAdapter extends RecyclerView.Adapter {
private ArrayList<Student> data;
ArrayList<Student> getData() {
return data;
}
void setData(ArrayList<Student> data) {
this.data = new ArrayList<>(data);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(RecyclerViewActivity.this).inflate(R.layout.itemview, null);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
MyViewHolder myViewHolder = (MyViewHolder) holder;
Student student = data.get(position);
myViewHolder.tv.setText(student.getNum() + "." + student.getName());
}
@Override
public int getItemCount() {
return data.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView tv;
MyViewHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.item_tv);
}
}
}
初期化データセット:
private void initData() {
students = new ArrayList<>();
Student s1 = new Student("John", 1);
Student s2 = new Student("Curry", 2);
Student s3 = new Student("Rose", 3);
Student s4 = new Student("Dante", 4);
Student s5 = new Student("Lunar", 5);
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
students.add(s5);
}
次にAdapterを実装し、RecyclerViewに設定する:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_view);
initData();
recyclerView = (RecyclerView) findViewById(R.id.rv);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new MyAdapter();
adapter.setData(students);
recyclerView.setAdapter(adapter);
}
これらの内容はすべて本編の内容ではありませんが、注意すべき点の一つはアダルトの定義です。
class MyAdapter extends RecyclerView.Adapter {
private ArrayList<Student> data;
ArrayList<Student> getData() {
return data;
}
void setData(ArrayList<Student> data) {
this.data = new ArrayList<>(data);
}
//
......
}
ここのsetData
方法は、直接ArayListの参照を保存するのではなく、ArayListを再構築し、まず覚えておいて、後になぜこのようにするのかを説明します。DiffUtilの使い方:
マウスを押すと、ArayListの内容を変更します。
public void change(View view) {
students.set(1, new Student("Fndroid", 2));
students.add(new Student("Jason", 8));
Student s2 = students.get(2);
students.remove(2);
students.add(s2);
ArrayList<Student> old_students = adapter.getData();
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new MyCallback(old_students, students), true);
adapter.setData(students);
result.dispatchUpdatesTo(adapter);
}
2−6行は集合を修正し、8行目は先にadapperの集合が古いデータを取得する。9行目の呼び出し
DiffUtil.calculateDiff
方法を見て、集合の違いを計算します。ここでは、CallBackインターフェースの実装クラス(計算のルールを指定するために)が導入され、新旧データをこのインターフェースの実現クラスに伝達します。最後にbollanタイプのパラメータがあります。このパラメータはMoveの検出が必要かどうかを指定します。必要でない場合、Itemが移動したら、先にremoveして、それからinsertだと思われます。ここでtrueとして指定していますので、移動効果があります。10行目に新しいデータをAdapterに再設定します。
第11行目は、9行目で得られたDiffResultオブジェクトの
dispatchUpdatesTo
を呼び出して、RecyclerViewに対して変化が発生したItemを更新するように通知します。ここでは上記の
setData
方法に戻ります。ここでは二つのセットを区別します。もしsetData
方法で直接参照を保存するならば、2−6行の修正で直接にAdapterのセットを修正します。Itemの移動をチェックしない設定であれば、効果は以下の通りです。
次に、CallBackインターフェースの実装クラスがどのように定義されているかを見ます。
private class MyCallback extends DiffUtil.Callback {
private ArrayList<Student> old_students, new_students;
MyCallback(ArrayList<Student> data, ArrayList<Student> students) {
this.old_students = data;
this.new_students = students;
}
@Override
public int getOldListSize() {
return old_students.size();
}
@Override
public int getNewListSize() {
return new_students.size();
}
// Item
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return old_students.get(oldItemPosition).getNum() == new_students.get(newItemPosition).getNum();
}
// Item , Item
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return old_students.get(oldItemPosition).getName().equals(new_students.get(newItemPosition).getName());
}
}
ここでは学号によって同じItemかどうかを判断し、名前によってこのItemが修正されているかを判断します。実際、このCallback抽象クラスにはもう一つの方法があります。この方法の役割は、この方法を通じて、全体の更新ではなく、このItemを部分的に更新することをAdapterに教えられます。
まず、このパスロードは何ですか?payloadはItemの変化を説明する対象です。つまり私達のItemにはどのような変化が発生しましたか?これらの変化は一つのpayloadにパッケージされています。だから私達は普通Bundelで働いてもいいです。
次に、
getChangePayload()
方法はgetChangePayload()
でtrueに戻り、areItemsTheSame()
がfalseに戻ったときに折り返しられたもの、すなわちItemの内容が変化したものであり、この変化は局所的なものである可能性がある(例えば、微博の点賛、Item全体ではなくアイコンを更新する必要がある)。したがって、areContentsTheSame()
にObjectをカプセル化してRecyclerViewにローカルリフレッシュを行うように教えることができる。上の例の中学校番号と名前が異なるTextViewで表示されていると仮定します。学号に対応する名前を修正した場合、部分的に名前を書き換えてもいいです。
まずCallbackの中のこの方法を書き換えることです。
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
Student newStudent = newStudents.get(newItemPosition);
Bundle diffBundle = new Bundle();
diffBundle.putString(NAME_KEY, newStudent.getName());
return diffBundle;
}
帰ってきたこの相手はどこで受け取りますか?実際にはgetChangePayload()
に二つのRecyclerView.Adapter
方法があります。一つは書き換えなければならないものです。もう一つの三つ目のパラメータは一つのパスロードのリストです。
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {}
ですから、私たちはAdapterでこの方法を書き直すだけで、Listが空であれば、元のonBindView HolderでItem全体の更新を行います。そうでなければ、payloadsの内容によってローカル更新を行います。
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
MyViewHolder myViewHolder = (MyViewHolder) holder;
Bundle bundle = (Bundle) payloads.get(0);
if (bundle.getString(NAME_KEY) != null) {
myViewHolder.name.setText(bundle.getString(NAME_KEY));
myViewHolder.name.setTextColor(Color.BLUE);
}
}
}
ここのpayloadsはnullではないので、直接に空かどうかを判断すればいいです。ここで注意します。RecyclerViewに大量のデータがロードされていると、アルゴリズムはすぐには完成しないかもしれません。ANRの問題に注意して、単独のスレッドを開いて計算することができます。
締め括りをつける
AndroidでDiffUtilを使って紹介しました。Androidの開発者たちの助けになりたいです。質問があれば、メッセージを書いて交流してください。