🐡

ListAdapterでRecyclerViewを効率的に使用する (DiffUtil, AsyncListDiffer)

2023/11/29に公開

RecyclerViewのListAdapterとは?

アンドロイドで同じレイアウトを繰り返しするリスト型UIを作るためにRecyclerViewを使います。 RecyclerViewを使うためには、Adapter、LayoutManager、ViewHolderの3つの準備物が必要です。 その中でAdapterはデータリストを実際の目でみえるようにitemViewに変換する役割をします。

Adapterの役割は3つ

  1. RecyclerViewに表示するデータリストの管理。
  2. Viewオブジェクトを再使用するためのViewHolderオブジェクトの作成。
  3. データリストでpositionに該当するデータをitemViewに表示。

この中でListAdapterを適用して3番を効率的に処理する方法についてまとめたいと思います。
RecyclerViewで表示するリストデータを全て更新する必要があるとき、このようなコードを使用するケースをよく見たと思います。

fun setItems(newItems: List<Item>) {
    items.clear()
    items.addAll(newItems)
    notifyDataSetChanged()
}

notify Data Set Changed()を呼び出すと、AdapterにRecycler Viewのリストデータが変わったので、すべての項目をアップデートしてくださいとう信号が届きます。

ところで、もし100個のitemが入っているリストをリロードした時、99個のデータはそのままなのに、たった1個だけデータの内容が変わったらどうなるか? 変更されたitemのpositionが分かるならnotify Item Changed(position)を使えばいいですが、その位置が無作為ならばこのメソッドは使えないです。 実質的に直すitemは1つだけだが、Adapterは知らないため、100個のitemをすべてアップデートすることになります。 このような不要な交換費用を減らすために考案されたのがDiffUtilです。

DiffUtil

DiffUtilはRecyclerViewの性能をさらに改善できるようにするユーティリティクラスです。 既存のデータリストと交換するデータリストを比べて、実質的にアップデートが必要なアイテムを選び出します。

使用方法

まず、DiffUtilが二つのリストの違いを計算する時に使用するDiffUtil.Callback()を作成しなければならないです。

areContentsTheSame()

2つのアイテムが同じ内容物を持っているかチェックします。
このメソッドは、are Items The Same() がtrue のときにのみ呼び出されます。

areItemsTheSame()

2つのアイテムが同じアイテムかチェックしあます。
例えば、itemが自分だけの固有のidのようなものを持っているなら、それを基準にすればいいです。

class BaseDiffUtil<T>(private val newList: List<T>, private val oldList: List<T>) : DiffUtil.Callback() {

    override fun getOldListSize(): Int = oldList.size

    override fun getNewListSize(): Int = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        newList[newItemPosition] == oldList[oldItemPosition]

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        newList[newItemPosition] == oldList[oldItemPosition]
}

DiffUtil.calculateDiff(diffUtil)でアップデートが必要なリストを探します。 notifyDataSetChanged()の代わりにdispatchUpdatesTo(Adapter adapter)を使えば、交換が必要なアイテムに対して部分的にデータを交換しろというnotifyが実行されます。

abstract class BaseRecyclerView<B : ViewDataBinding, T : Any>(
    @LayoutRes private val layoutResId: Int
) : RecyclerView.Adapter<BaseViewHolder<B, T>>() {
    protected val items = mutableListOf<T>()

    fun setItems(newItems: List<T>) {
        val diffUtil = BaseDiffUtil(items, newItems)
        val diffResult = DiffUtil.calculateDiff(diffUtil)

        items.clear()
        items.addAll(newItems)
        diffResult.dispatchUpdatesTo(this)
    }

  // ...
}

アイテム数が多い場合、比較演算時間が長くなる可能性があるため、calculateDiffはバックグラウンドスレッドで処理しなければならない。
具現するときの制約でDiffUtilが処理できるリストの最大サイズは2²⁶です。

AsyncListDiffer

DiffUtilをより単純に使用できるクラスです。自動的にマルチスレッドに対する処理がされているため、開発者が直接同期処理を行う必要がなくなります。 AsyncDifferConfigでbackgroundThreadExecutorを別に設定しない場合は、Executors.newFixedThreadPool(2)でスレッドプールを1つ作って比較演算を処理します。

使用方法

まず、アイテムを比較する時に呼び出すDiffUtil.ItemCallbackを作成します。

class PlaceDiffUtilCallback : DiffUtil.ItemCallback<Place>() {

    override fun areItemsTheSame(oldItem: Place, newItem: Place) =
        oldItem.place.id == newItem.place.id

    override fun areContentsTheSame(oldItem: Place, newItem: Place) =
        oldItem.place == newItem.place
}

Adapter内部にAsyncListDifferオブジェクトを宣言して使ったらいいです。

getCurrentList()

adapterで使用するitemリストにアクセスしたい場合使ったらいいです。

submitList(Listist newList)

リストデータを交換する際に使ったらいいです。

class PlaceRecyclerAdapter : RecyclerView.Adapter<PlaceViewHolder>() {
    private val diffUtil = AsyncListDiffer(this, PlaceDiffUtilCallback())

    fun replaceTo(newItems: List<Place>) = diffUtil.submitList(newItems)

    fun getItem(position: Int) = diffUtil.currentList[position]

    override fun getItemCount(): Int = diffUtil.currentList.size

    override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) =
        holder.bind(getItem(position))

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PlaceViewHolder(
        ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )
}

ListAdapter

使いやすかったAsyncListDifferをさらに手軽に使えるようにしてくれます。
上記の例を見ると、外部からアイテムリストを交換するreplaceTo()、特定のポジションのアイテムを返却するgetItem()など、RecyclerView.Adapterインターフェースに合わせて開発者が自ら具現しなければならない機能があります。これを作成必要がないようにしてくれるのがListAdapterです。
ListAdapterはAsyncListDifferのwrapperクラスで、RecyclerView.Adapter<>を実装していいます。これを使用すれば、RecyclerView Adapterコードがすごく短くなります。

使用方法

getCurrentList()

現在のリストを返却

onCurrentListChanged()

リストがアップデートされた時に実行するコールバック指定

submitList(Mutable Listlist)

リストデータを交換する際に使用

class PlaceRecyclerAdapter : ListAdapter<Place, PlaceViewHolder>(PlaceDiffUtilCallback()) {

    override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) =
        holder.bind(getItem(position))

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PlaceViewHolder(
        ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )
}

外部からadapterを使用する方法

val placeAdapter = PlaceRecyclerAdapter()
placeAdapter.submitList(newItems) // アイテムアップデート

Discussion