KotlinTips: ListAdapter(RecyclerView)

概要

RecyclerViewで使用するAdapterクラスは以前はRecyclerView.Adapterを継承したものが一般的でした。
RecyclerView.Adapterクラスでは少なくとも3つのメソッドをOverrideする必要があります。
実装の詳細は以前の投稿にあります。

一方で最近ではListAdapterを継承して実装する機会が多いでしょう。
ListAdapterでは少なくとも2つのメソッドをOverrideし、その他にListAdapterで保持するデータを比較するDiffUtil.ItemCallbackクラスを実装する必要があります。

class SampleRecyclerViewAdapter(itemDiffCallback : SampleItemDiffCallback) :
    ListAdapter<SampleData, SampleViewHolder>(itemDiffCallback) {}

ListAdatperはMVVMアーキテクチャとLiveDataクラスを使用した場合にその本領を発揮しますが、それ以外の場合でも開発者の負担を減らします。

ListAdapterの使い方を整理してみたいと思います。
ListAdapter以外の部分については以前の投稿にあります。

サンプル実装はGitHubにあります。

サンプル実装

参考資料は主に公式サイトですが下記にまとめてあります。

Adapterクラス

以前はAdapterクラスで保持するデータリストに変更があった場合、notifyメソッドを利用しAdapterクラスに通知する必要がありました。
データリストの変更はnotifyAllメソッドを通してAdapterクラスに一括して通知することができますが、全てのコンポーネントを作り直すため非効率であり、データリストの変更は開発者が管理しnotifyItemMovedやnotifyItemRemovedを用いて個別に通知することが推奨されていました。

    //アイテムを移動
    fun moveItem(fromPosition: Int, toPosition: Int) {
        val item = itemList.removeAt(fromPosition) //データをリストから削除
        itemList.add(toPosition, item) //削除したデータを指定された位置に挿入
        notifyItemMoved(fromPosition, toPosition) //Adapterに反映
    }

    //アイテムを削除
    fun removeItem(position: Int) {
        itemList.removeAt(position) //データを削除
        notifyItemRemoved(position) //Adapterに反映
    }

開発者の管理が適切でない場合、データリストで保持している実際のデータとAdapterクラスで管理しているデータの個数や添え字が一致せずに、IndexOutOfBounds例外が発生したり、画面を再描画するとデータが変更前のデータに戻っていたりと不具合の原因になりがちでした。

ListAdapterではデータリストのデータが更新された場合notifyメソッドを使用するのではなく、ListAdapterのsubmitListメソッドを使いデータリストを丸ごとAdapterクラスに渡すようになりました。

    //アイテムを移動
    fun moveItem(fromPosition: Int, toPosition: Int) {
        val newList = currentList.toMutableList() //Mutableなリストを作成
        val item = newList.removeAt(fromPosition) //データをリストから削除
        newList.add(toPosition, item) //削除したデータを指定された位置に挿入
        submitList(newList) //データを更新
    }

    //アイテムを削除
    fun removeItem(position: Int) {
        val newList = currentList.toMutableList() //Mutableなリストを作成
        newList.removeAt(position) //データを削除
        submitList(newList) //データを更新
    }

ListAdapterではデータを比較するAsyncListDifferクラスを実装しましたが、内部ではこのクラスを通してどのデータが更新されたのかをチェックし、更新のあったデータのみ再構築します。
その結果、開発者が開発者がデータリストの変更を個別に管理する必要がなくなり、非効率な更新処理もなくなりました。

DiffUtil.ItemCallbackクラス

DiffUtil.ItemCallbackクラスは抽象クラスなので継承したクラスを作成します。
Overrideするメソッドは2つあります。

areItemsTheSame(oldItem, newItem)

このメソッドでは引数で渡される2つのオブジェクトoldItem、newItemが同じアイテムを表すかどうかを判定する処理を記述します。
一般的にはIDの比較、または同じインスタンスであるかなどの比較処理を行います。

areContentsTheSame(oldItem, newItem)

このメソッドはareItemsTheSame()がtrueを返す場合、つまり引数で渡される2つのオブジェクトが同じアイテムを表していると判定された場合にのみ呼び出されます。

このメソッドでは2つのオブジェクトoldItem、newItemが同じデータ内容であるかを判定します。
一般的にはoldItemとnewItem、それぞれが保持しているデータに変更がないか比較します。
このメソッドでfalseを返す場合Viewは再構築せずに再利用されます。

ListAdapterのデータを変更したい場合

ListAdapter内部で保持しているデータはcurrentListプロパティから取得できますが、このデータはイミュータブル(変更不可)になっています。

これはアプリで何らかの処理を行いデータベースを更新し、更新後のデータリストを取得、そのデータリストをListAdapterのsubmitListに渡して画面描画を更新、という設計が想定されているからでしょう。

データベースなどを使用せず、アプリを起動する度にデータリストを初期化する場合もあるでしょう。
その場合データを変更する為にイミュータブル(変更不可)なリストからミュータブル(変更可)なリストを作成し、データを変更後submitListメソッドに渡します。

参考資料

サンプルプログラム

公式サイト

公式サンプルプログラム

Advertisements