この記事はAndroid Advent Calendar 2017 の2日目の記事です。
はじめに
RecyclerViewのデータ更新をする際どう実装していますか?
notifyDataSetChanged()
などnotify系のメソッドを使って更新することば多い思います。しかしこのnotify系のメソッドは、画面に表示されているViewと上下にあるバッファのViewすべてを更新します。これでは、1行更新したいとなったときに無駄が生じてしまいます。
SupportLibrary25.1.0で追加されたDiffUtilをつかうことでパフォーマンスの良いコードを簡潔に書くことが出来るようになります。
DiffUtilとは
DiffUtilは2つのListを比較して差分を計算するユーティリティクラスです。
EUGENE W. MYERSの差分アルゴリズムを使って計算していて、計算時間はO(N)
(移動検出をする場合はO(N ^ 2)
)になります。
DiffUtilをRecyclerViewで使うことで、変更があった際に差分だけ更新され、更新された部分にアニメーションがかかるようになります。最高ですね
DiffUtilを使うときにはCallbackとDiffResultを使います。以下でこの二つについて説明していきます。
DiffUtil.Callback
Callbackは、2つのList間の差分を計算するために使用されるCallbackクラスです。
5つのメソッドが生えていて、うち4つがabstractとなっています。DiffUtilはこれらのメソッドを使って差分を計算します。
戻り値 | メソッド名 | 意味 |
---|---|---|
abstract int | getOldListSize() | 古いリストのサイズを返します。 |
abstract int | getNewListSize() | 新しいリストのサイズを返します。 |
abstract boolean | areItemsTheSame(int oldItemPosition, int newItemPosition) | 2つのアイテム自体が同じものであるかを判定します。 |
abstract boolean | areContentsTheSame(int oldItemPosition, int newItemPosition) | 2つのアイテムのデータ内容が同じであるかを判定します。 |
Object | getChangePayload(int oldItemPosition, int newItemPosition) |
areItemsTheSame がtrue を返し、areContentsTheSame がfalse の時に呼び出され、どんな変更があったかを知ることが出来ます。 |
class RecyclerDiffCallback(private val old: List<Any>, private val new: List<Any>) : DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size
override fun getNewListSize(): Int = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
= old[oldItemPosition] == new[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
= old[oldItemPosition].id == new[newItemPosition].id
}
DiffUtil.DiffResult
fun update(new: List<Any>) {
val diffResult = DiffUtil.calculateDiff(RecyclerDiffCallback(old, new))
diffResult.dispatchUpdatesTo(this)
}
calculateDiff()
から帰ってきたDiffResultのdispatchUpdatesTo(adapter)
を呼び出すことでAdapterに変更が通知されます。
非同期で計算を行う
上のコードを使うことでDiffUtilを用いてRecyclerViewを更新できるようになります。
しかし、DiffUtilはListのサイズが大きい場合や複雑な場合には、計算に時間がかかるためバックグラウンドスレッドが推奨されています。そこで、RxJavaを用いて非同期でDiffUtilをあつかうようにします。
private val subject: Subject<List<Any>> = PublishSubject.create<List<Any>>().toSerialized()
subject.observeOn(Schedulers.computation())
.map { Pair<List<Any>, List<Any>>(it, mutableListOf()) }
.scan { old, new -> Pair(old.first, new.first) }
.map { (oldList, newList) -> RecyclerDiffCallback(oldList, newList) }
.map(DiffUtil::calculateDiff)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
it.dispatchUpdatesTo(this)
}, {
it.printStackTrace()
})
ここでポイントとなるのは、以下の3点です。
SerializedSubject
Pair
.scan()
SerializedSubject
PublishSubjectを使うことでListの更新を監視することが出来ます。
しかし、RxJavaはスレッドセーフではなく、このままでは別スレッドから更新があった時にクラッシュする可能性があります。そこで、.toSerialized()
してSerializedSubjectにすることで解決します。
Pair
RxJavaでDiffUtilを使うためには、新しいListと、calculateDiff()
を呼び出すためのDiffResultをStreamに流す必要があります。そこで、 PairでMutableListとDiffResultをラップしてStreamに流しています。
PublishSubject.create<Pair<MutableList<Any>, DiffUtil.DiffResult?>>().toSerialized()
.scan()
DiffUtilで差分を計算するためには古いListと新しいリストが必要です。
そこで、RxJavaの.scan()
を使います。
RxJavaのscan()は前回の値を引数にとるOperatorです。このscanを使うことで前回の値と今回の値を扱うことが出来るようになります。
さいごに
これで、RecyclerViewをパフォーマンスよく更新することが出来るようになります。
また、これらの処理をまとめて行うAbstractのBaseAdapterをつくると便利です。機能に応じた各AdapterがこのBaseAdapter継承するようにすることで使う側は何も考えずにAdapterを実装することが出来るようになります。
/**
* RecyclerViewを使う場合に継承必須のAdapter
*
* @author nakker1218
*/
abstract class RecyclerBaseAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
protected var items: MutableList<Any> = mutableListOf()
private val subject: Subject<List<Any>> = PublishSubject.create<List<Any>>().toSerialized()
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
init {
subject.observeOn(Schedulers.computation())
.map { Pair<List<Any>, List<Any>>(it, mutableListOf()) }
.scan { old, new -> Pair(old.first, new.first) }
.map { (oldList, newList) ->
object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldList[oldItemPosition] == newList[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = areContentsTheSame(oldList, newList, oldItemPosition, newItemPosition)
}
}
.map(DiffUtil::calculateDiff)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
it.dispatchUpdatesTo(this)
}, {
it.printStackTrace()
})
.also { compositeDisposable.add(it) }
}
abstract fun areContentsTheSame(oldList: List<Any>, newList: List<Any>, oldItemPosition: Int, newItemPosition: Int): Boolean
override fun getItemCount(): Int = items.size
fun addAll(items: List<Any>) {
items.addAll(items)
subject.onNext(items)
}
fun destroy() {
compositeDisposable.clear()
}
}
参考
https://developer.android.com/reference/android/support/v7/util/DiffUtil.html
https://medium.com/@iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00
https://android.jlelse.eu/smart-way-to-update-recyclerview-using-diffutil-345941a160e0
https://hellsoft.se/a-nice-combination-of-rxjava-and-diffutil-fe3807186012
http://blog.takuji31.jp/entry/kanmoba17
https://qiita.com/ralph/items/f7205c8171826cc2153b
https://qiita.com/izumin5210/items/952a23bf065648feabb7