とある勉強会でコードがほしいと言われたので書いてみます。
DiffUtil
24.2.0で追加された、2つのリストの差分計算を行うユーティリティーです。
計算量がO(N)
、移動検知ありだとO(N^2)
です。
calculateDiff
の結果をdispatchUpdatesTo(adapter)
とすることで、RecyclerView.Adapter
の各種notify系が呼ばれ、いい感じにアニメーションなどをやってくれます。
使用方法はドキュメントを参照してください。
Diffable
DiffUtil.calculateDiff
にはDiffUtil.Callback
が必要です。
これをAdapterごとに実装するのは面倒なので、比較可能なアイテムを表すインターフェースとCallbackの実装クラスを定義します。
好みは別れると思いますが、ついでにグローバルスコープ関数も作ります。
interface Diffable {
// otherと同じIDを持つかどうか
fun isTheSame(other: Diffable): Boolean = equals(other)
// otherと完全一致するかどうか
fun isContentsTheSame(other: Diffable): Boolean = equals(other)
}
private class Callback(
val old: List<Diffable>,
val new: List<Diffable>
) : DiffUtil.Callback() {
override fun getOldListSize() = old.size
override fun getNewListSize() = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition].isTheSame(new[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition].isContentsTheSame(new[newItemPosition])
}
}
fun calculateDiff(
old: List<Diffable>,
new: List<Diffable>,
detectMoves: Boolean = false
): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(Callback(old, new), detectMoves)
}
Delegates.observableと組み合わせる
差分計算するには、新旧のList
が必要になりますが、KotlinにはDelegates.observable
があるので、これを使います。
class MyAdapter : RecyclerView.Adapter<AbstractViewHolder>() {
var items: List<Item> by Delegates.observable(emptyList()) { _, old, new ->
calculateDiff(old, new).dispatchUpdatesTo(this)
}
}
上記のようにAdapter
にvar items: List<Item>
プロパティを作ります。
MyAdapter.items
に値をセットすると、自動でDiffの反映までやってくれます。
ViewHolder周りは省略します。
実装例
Item
にはDiffable
を実装します。
デフォルト実装があるので、isTheSame
のみオーバーライドします。
data class Item(val id: Long, val fullName: String) : Diffable {
override fun isTheSame(other: Diffable) = id == (other as? Item)?.id
}
続いてActivityなどの実装例です。
細かいのは省略してます。
val adapter = MyAdapter()
recyclerView.adapter = adapter
adapter.items = listOf(Item(0, "Item: 0"))
recyclerView.postDelayed({ adapter.items += Item(1, "Item: 1") }, 1000)
上記コードだと、最初はItem: 0
のみ表示されているが、1秒後にItem: 1
がアニメーションとともに表示されます。
もう少し実用的なサンプル
こちらです。
ReduxでGitHubのリポジトリを表示するだけのものです。