とある勉強会でコードがほしいと言われたので書いてみます。
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のリポジトリを表示するだけのものです。