RecyclerViewにListAdapterを使って差分更新を実現する

TL;DR

RecyclerViewを用いた画面で、ListAdapterを使用して差分更新を実現したので方法をまとめます。

ListAdapterとはなにか?

下記のリンクがListAdapterリファレンスです。 https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter

パッケージはandroid.support.v7.recyclerview.extensionsとなり、Support Library 27.1.0から実装されています。

ListAdapterAsyncListDifferをラップしたRecyclerView.Adapterの実装です。 AsyncListDifferは与えられたListの差分更新をバックグラウンドスレッドで実行し、差分が存在する場合、RecyclerView.Adapterの更新通知(notifyItemRangeInsertedなど)の呼び出しを行います。

なにが嬉しいのか?

AACのViewModelとLiveDataを持ちいたMVVMのアーキテクチャを採用した場合、ViewModel側にLiveData<List<Hoge>>のような形でリスト要素をプロパティとして持たせた上でView側(Activity/Fragment)でLiveDataを監視、変更があれば変更されたListを用いてRecyclerViewを更新という実装をするかと思います。

このときに、単純にRecyclerView.Adapterに対して更新されたListを渡した場合、すべての要素が更新されますが、ListAdapterを使用した場合は新しく取得したListと前回ListAdapterに設定したListの差分となる部分のみRecyclerViewの更新が行われる形となり、更新処理の高速化が見込まれます。

DiffUtil.ItemCallbackの実装

まず、ListAdapterを使用するためにはListに含まれる要素を比較する為のDiffUtil.ItemCallback<ITEM>の実装が必要なので、そちらを実装します。

DiffUtil.ItemCallback<ITEM>の定義は以下を参照ください。 https://developer.android.com/reference/android/support/v7/util/DiffUtil.ItemCallback

実装すべきメソッドは以下の2つになります。

areContentsTheSame(T oldItem, T newItem)

oldItemとnewItemの値が同じ場合にtrue

areItemsTheSame(T oldItem, T newItem)

oldItemとnewItemが同じ要素(DBのカラムなど)を参照している場合にtrue

実装例は以下のようになります。

private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Book>() {
    override fun areContentsTheSame(oldItem: Book?, newItem: Book?) = 
        oldItem == newItem
    override fun areItemsTheSame(oldItem: Book?, newItem: Book?) = 
        oldItem?.id == newItem?.id
}

ListAdapterの実装

次にListAdapter自体の実装ですが、こちらはコンストラクタに実装済みのDiffUtil.ItemCallbackインスタンスを渡すようにすればOKです。

class MyAdapter(val inflater: LayoutInflater) : ListAdapter<MyItem, MyItemViewHolder>(DIFF_CALLBACK) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) {
        …
    }

    override fun onBindViewHolder(holder: MyItemViewHolder, position: Int) {
        holder.bindTo(getItem(position));
    }
}

Activity/Fragmentからの呼び出し

最後にActivity/FragmentからAdapterに値を反映させる場合はsubmitList()の呼び出しを行います。

viewModel.itemList.observe(this, Observer{ adapter.submitList(it) })