Takuji->find;

Sansan株式会社でAndroidのテックリードをやってます、技術的な記事を書いているつもり

Support Library 24.2.0で追加されたDiffUtilを試してみた

こんばんは、最近は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)

areItemsTheSametrue だった時に呼ばれる。

areItemsTheSame はアイテムそのものが同じものであるかどうかの判定だったが、 areContentsTheSame はアイテムの内容が同じかどうかを判定する。

見た目上なんらかの変化があれば false を返す。

getChangePayload(int, int)

areContentsTheSamefalse だった時に呼ばれる。

古いアイテムと新しいアイテムで、どういった変更があったか通知するオブジェクトを生成する。

通知するオブジェクトの型は自由なので、変更通知用のクラスを作ってそのオブジェクトを渡すイメージ。

このメソッドだけ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にあげてます。

github.com