Android開発のDiffUtilの使用詳細

9118 ワード

前に書いたら
ディ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の開発者たちの助けになりたいです。質問があれば、メッセージを書いて交流してください。