こんばんは、最近はAndroid JavaではなくPerlとTypeScriptを書いているid:takuji31です。
この記事は本日開催の関西モバイルアプリ研究会 #17の発表を元に作成しています。
今日はSupport Library 24.2.0でrecyclerview-v7に追加された DiffUtil
を試してみたので紹介します。
DiffUtilとは
2つの List
の差分を計算するユーティリティー。
List
の要素ごとの変化を計算する DiffUtil.Callback
を引数に取り、 DiffUtil.DiffResult
を受け取る。
デフォルトでは追加と削除と更新のみ受け取れるが、オプション指定することで移動も計算できる。ただし、計算コストが上がる。
DiffUtil.Callback
それぞれの要素の変化を DiffUtil
に伝えるCallback。5つのメソッドが用意されていて、4つがabstractになっている。
getNewListSize() / getOldListSize()
名前通り新しい/古い List
のサイズを返す。
areItemsTheSame(int, int)
2つのアイテムが同じものであるかどうかを判定する。
例えば2つの List
にユーザーのエンティティーが入っていると仮定して、それぞれのユーザーが同一のものかをこのメソッドで判定してやる。
areContentsTheSame(int, int)
areItemsTheSame
が true
だった時に呼ばれる。
areItemsTheSame
はアイテムそのものが同じものであるかどうかの判定だったが、 areContentsTheSame
はアイテムの内容が同じかどうかを判定する。
見た目上なんらかの変化があれば false
を返す。
getChangePayload(int, int)
areContentsTheSame
が false
だった時に呼ばれる。
古いアイテムと新しいアイテムで、どういった変更があったか通知するオブジェクトを生成する。
通知するオブジェクトの型は自由なので、変更通知用のクラスを作ってそのオブジェクトを渡すイメージ。
このメソッドだけabstractになっていないので、実装しなくてもよい。その場合は null
が渡される。
DiffUtil.DiffResult
DiffUtil.DiffResult
は計算したdiffの結果を保持している。直接結果を読むことはできず、 ListUpdateCallback
あるいは RecyclerView.Adapter
を経由して取得する。
RecyclerView.Adapter
に新しいデーターを渡した後に DiffResult.dispatchUpdatesTo(RecyclerView.Adapter adapter)
を呼ぶことで、 RecyclerView
の変更通知に使える。
使ってみる
※いつも通りKotlinです
DiffUtil
はrecyclerview-v7の24.2.0以降に入っているので、入れておく。
雑にModelを作る
enum class Status { INTERESTED, LIKE, LOVE; } data class Artist(val name: String, val status: Status) { companion object { val list: List<Artist> = listOf( Artist(name = "小倉唯", status = LOVE), Artist(name = "雨宮天", status = LOVE), Artist(name = "水瀬いのり", status = LIKE), Artist(name = "Trysail", status = LIKE), Artist(name = "Minami", status = LIKE), Artist(name = "佐倉綾音", status = LOVE), Artist(name = "田村ゆかり", status = INTERESTED), Artist(name = "ワルキューレ", status = INTERESTED), Artist(name = "水樹奈々", status = INTERESTED) ) } }
DiffUtil.Callback
の実装を作る、匿名クラスでも問題ないが、ちゃんとクラス化しておくと使い回せるのでよさそう。
class DiffCallback(val oldList: List<Artist>, val newList: List<Artist>) : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].name == newList[newItemPosition].name } override fun getOldListSize(): Int { return oldList.size } override fun getNewListSize(): Int { return newList.size } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Pair<Status, Status> { return Pair(oldList[oldItemPosition].status, newList[newItemPosition].status) } }
このケースでは name
をIDととらえて、 name
が同じなら同じアイテムで、かつ他のプロパティー(といっても status
だけだが)が一致した場合に同じコンテンツとみなしている。
あとは適当に RecyclerView.Adapter
やら Activity
やら必要なものを実装してやって、リストの更新をするときに以下のように実行してやる。
fun updateItems(items : List<Artist>) { val oldItems = adapter.items val diffResult = DiffUtil.calculateDiff(DiffCallback(oldList = oldItems, newList = items), true) adapter.items = items diffResult.dispatchUpdatesTo(adapter) }
するといい感じにリストの更新がアニメーションされる。
RecylerView
以外と組み合わせる、Change payloadの中身を見て何かしたい、などといった場合は ListUpdateCallback
と組み合わせて使うこと。
制限事項
- 古い
List
と新しいList
の中身を比較するので、List
の要素が変わる、あるいは要素そのものの値が自動更新されるような仕組み(Realmとか)とは一緒に使えなさそう List
の要素数は2^26(=67108864)まで
最後に
DiffUtil
を使うことでRecylerView
への詳細な更新通知が楽になる- 手で更新通知投げるの地味に面倒だったが、これだと
DiffResult.dispatchUpdatesTo(RecyclerView.Adapter adapter)
呼ぶだけでよい - パフォーマンスが気になるが、全部同期的に実行されるので、Rxなんかを使ってバックグラウンドで実行してやるとよさそう
RecylerView
以外との組み合わせなど凝ったことをやりたい時はListUpdateCallback
を使う- Realmなど自動更新されるようなものとの組み合わせは悪そうだが、ViewModelを挟んでやるとよいのではないか
サンプルコードはGithubにあげてます。