2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel 〜

# tl;dr

* UIのリスト表示は、{RecyclerView + androidx.recyclerview.widget.ListAdapter + DataBinding + LiveData + ViewModel} on Kotlinで構築する
* ListAdapterに引数として何を渡すかは要件（やりたいこと）による。最小ケースでは引数なしもありえる
* executePendingBindings() is 大事
* LayoutManager is 大事
* 今回のサンプルのリポジトリー：https://github.com/quwac/howtouserecyclerview2020

# この記事は？

RecyclerViewの使い方のメモです。
2020年時点のRecyclerView実装の記事の中ではベストな方法、かつユースケースが網羅的になるように書きます。

個々の技術要素の使い方よりも技術要素同士をどう組み合わせるかに主眼を置いています。
DataBinding、LiveData、ViewModel、Kotlinそのものの使い方については別途検索することをおすすめします。

# 作るもの

![video.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82681/08f1509e-5eca-7def-19c6-f3c01c0a333f.gif)

一般的な（？）リストUIです。
＋ボタンを押すとリストの要素が追加されていきます。

リストの要素をタップすることができます。
タップにはリップルエフェクト（タップした点を中心に、もわぁ〜んと広がるUI効果）がついています。

特色としてリストの各要素にスイッチをつけています。
ユーザー操作によってスイッチのON/OFFが切り替わると、即座にデータにも反映されます。
✅ボタンを押すとリストの全要素のスイッチがONになります。

# 実装

## build.gradle

モジュールの方のbuild.gradleです。

```build.gradle
...
+ // 後述するlifecycle-common-java8のために必要
+ apply plugin: "kotlin-kapt"

android {
    ...

+   // DataBindingを有効にする。
+   // 少し前まではdataBinding.enabled = trueだったが最近はその書き方はdeprecatedになった。
+   buildFeatures.dataBinding = true

    ...

+   // DataBindingによる自動生成コードはJava 8以上でないと生成できないため指定する。
+   compileOptions {
+       sourceCompatibility JavaVersion.VERSION_1_8
+       targetCompatibility JavaVersion.VERSION_1_8
+   }
+
+   kotlinOptions.jvmTarget = "1.8"

    ...
}

...

dependencies {
    ...

+   // RecyclerViewを使うために追加
+   // https://developer.android.com/jetpack/androidx/releases/recyclerview?hl=ja
+   implementation 'androidx.recyclerview:recyclerview:1.1.0'

+   // ViewModelとLiveDataを使うために追加
+   // https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ja
+   // 数が多くて漏れが生じやすいので必ず上のリファレンスからコピペする。
+   // コピペ時はlifecycle-common-java8だけ注意する（後述）。
+   // Android Studioでプロジェクトを新規作成するとデフォルトの構成でなんとなくビルドが通ってしまうことがあるが、
+   // 正常に動作しないので必ず手で追加する。
+   def lifecycle_version = "2.2.0"
+   implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+   implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+   // Java 8を使う時はlifecycle-compilerはdependenciesに加えず、lifecycle-common-java8を加える。
+   // また上記リファレンスだとimplementationになっているが、kaptに変更する。
+   kapt "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

    ...

+   // 以下は本筋とは関係ないが要るライブラリー
+
+   // Fragment内でviewModels関数を使うために追加
+   implementation "androidx.fragment:fragment-ktx:1.2.5"
+   // ConstraintLayoutを使うために追加
+   implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+   // SwitchMaterialを使うために追加
+   implementation 'com.google.android.material:material:1.2.0'

}

```

## エンティティクラス `User`

エンティティクラス `User` です。リストに表示される要素に対応します。

```User.kt
data class User(
    val id: Long,
    val name: String,
    val isChecked: MutableLiveData<Boolean> = MutableLiveData(false)
) {
    ...
}
```

ユーザー操作によるスイッチのON/OFFは即座に反映させたいので、MutableLiveData<Boolean>の `isChecked` で持たせています。

ちなみに、即座に反映させるための仕組みとしてかつては `ObservableBoolean` や `ObservableField` といった `BaseObservable
` がありましたが、2020年現在は `LiveData` に取って代わられています。
最早 `BaseObservable` を新規に使うことはないでしょう。

## ViewModelクラス `MainViewModel`

ViewModelクラス `MainViewModel` です。 `ListAdapter` に渡すリストを抱えています。
リスト表示や要素の状態変更に関係のないコードは端折っています。

```MainViewModel.kt
class MainViewModel : ViewModel() {

    private val usersRaw = mutableListOf<User>()

    private val _users = MutableLiveData<List<User>>(emptyList())
    val users: LiveData<List<User>> = _users.distinctUntilChanged()

    ...

    fun addElement() {
        // userの作成
        // ...

        usersRaw.add(user)
        _users.value = ArrayList(usersRaw)

        // ...
    }

    fun checkAll() {
        usersRaw.forEach {
            it.isChecked.value = true
        }
    }

    fun onClickItem(item: User) {
        // ...
    }
}
```

`onClickItem` に注目してください。
今回、リスト要素のタップイベント（コールバック）を受け取るのはViewModelになります。
つぎの `user_view.xml` でも説明します。

## レイアウトファイル `main_fragment.xml` 、 `user_view.xml`

レイアウトファイル `main_fragment.xml` 、 `user_view.xml` です。
それぞれ、画面のレイアウト、リスト要素のレイアウトになります。

```main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="<package>.ui.main.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.main.MainFragment">

        ...

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView">

        </androidx.recyclerview.widget.RecyclerView>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/floatingActionButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            android:clickable="true"
            android:focusable="true"
            android:onClick="@{() -> viewModel.addElement()}"
            android:src="@drawable/ic_baseline_add_24"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/floatingActionButton2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            android:clickable="true"
            android:focusable="true"
            android:onClick="@{() -> viewModel.checkAll()}"
            android:src="@drawable/ic_baseline_check_24"
            app:layout_constraintBottom_toTopOf="@+id/floatingActionButton"
            app:layout_constraintEnd_toEndOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

```

`main_fragment.xml` は特別なことはしていません。
いつもどおりに variable `viewModel`のプロパティや関数を `"@{...}"` でbindingしています。

```user_view.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="user"
            type="io.github.quwac.how_to_use_recyclerview_2020.ui.main.User" />

        <variable
            name="viewModel"
            type="io.github.quwac.how_to_use_recyclerview_2020.ui.main.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:background="?android:attr/selectableItemBackground"
        android:onClick="@{() -> viewModel.onClickItem(user)}">

        <TextView
            android:id="@+id/name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{user.name}"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"
            app:layout_constraintEnd_toStartOf="@+id/isEnabled"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.switchmaterial.SwitchMaterial
            android:id="@+id/isEnabled"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="64dp"
            android:checked="@={user.isChecked}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

一方、 `user_view.xml` は少し目新しさがあるかもしれません（？）。
variableとして `user` だけでなく `viewModel` も指定しています。これはリスト要素タップのコールバッククラスとして `MainViewModel` を指定しているためです。どの要素がタップされたかは、`user` を指定します。これにより、レイアウトファイルだけでタップのコールバック処理を完結させることができます。

その他に着目すべき点としては `isEnable` の `android:checked` に指定された `user.isChecked` でしょうか。
前述したとおり、 `User#isChecked` は `MutableLiveData<Boolean>` です。
MutableLiveDataを意図したとおり動作させるためには、bindingオブジェクトに `LifecycleOwner` をセットしてやる必要があり、そのため `UserListAdapter` に `LifecycleOwner` オブジェクトを渡してやる必要があります。これは後ほど説明します。

リップルエフェクトは `user_view.xml` で指定します。
ルート要素のレイアウトに `android:background="?android:attr/selectableItemBackground"` を指定してください。

## ListAdapterクラス `UserListAdapter`、 `DiffUtil.ItemCallback` 、 ViewHolderクラス `UserViewHolder`

ListAdapterクラス `UserListAdapter` です。`androidx.recyclerview.widget.ListAdapter` の継承クラスになります。

ようやく本丸です。

ネット検索をしていると、アダプタークラスについては冗長な実装や古いやり方の実装を多数見かけます。
「個々の技術要素の使い方は説明しない」と書きましたが、ここでは少しだけ丁寧に見ていきます。

はじめにファイルの全景を引用します。
ここではobject `DiffCallback` と ViewHolderクラス `UserViewHolder` も定義しています。

```UserListAdapter.kt
private object DiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem == newItem
    }

}

class UserListAdapter(
    private val viewLifecycleOwner: LifecycleOwner,
    private val viewModel: MainViewModel
) : ListAdapter<User, UserListAdapter.UserViewHolder>(DiffCallback) {

    class UserViewHolder(private val binding: UserViewBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: User, viewLifecycleOwner: LifecycleOwner, viewModel: MainViewModel) {
            binding.run {
                lifecycleOwner = viewLifecycleOwner
                user = item
                this.viewModel = viewModel

                executePendingBindings()
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return UserViewHolder(UserViewBinding.inflate(layoutInflater, parent, false))
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position), viewLifecycleOwner, viewModel)
    }

}
```

### ListAdapterとDiffUtil.ItemCallback

`ListAdapter` は `RecyclerView.Adapter` を継承したクラスです。
かつては `RecyclerView.Adapter` を継承するのが主流でしたが、現在はゴリゴリにパフォーマンスチューニングするのでもない限り、 `ListAdapter` を継承するのが一般的です。
というのも、かつてのアダプタークラスといえば

* ①overrideするべき関数がたくさんある
* ② `val items: List<User>` のように、リストUIに表示する要素のリスト変数をアダプタークラス内で別途保持しなければならない
    * ③あまつさえ、外部クラスからの要請に応えリスト変数の中身をいい感じにデータ管理する必要がある
* ④`notifyDataSetChanged` や `notifyItemXXX` 関数を適切に呼ばないと処理が遅かったりリスト更新のアニメーションがいまいち

という非常に開発者泣かせのクラスでしたが、 `ListAdapter` では

* ① -> **最低限overrideしなくてはいけない関数は2つだけになった**
* ②、③ -> **リスト変数を保持する必要がなくなった**
* ④ -> **notifyDataSetChangedもnotifyItemXXXも一切呼ぶ必要がなくなった**

というように、劇的な改善が果たされました。ここに飛びつかない理由はありません。

必要なくなった処理はすべて `ListAdapter` 内で受け持ってくれるようになりました。その代わりに `DiffUtil.ItemCallback` 型の変数をコンストラクタに渡す必要ができました。

```kotlin
class UserListAdapter(
    ...
) : ListAdapter<User, UserListAdapter.UserViewHolder>(DiffCallback) {
```

`DiffUtil.ItemCallback` は2つの要素を比較するユーティリティクラスです。`ListAdapter` は要素の追加・変更・削除をこのユーティリティを使うことによって検知し処理してします。

```kotlin
private object DiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem == newItem
    }
}
```

`areItemsTheSame` は「2つの要素の識別子の同一性」を調べる関数です。多くの場合、エンティティクラスはidプロパティを持っているはずなのでそれを比較すればよいです。
`areContentsTheSame` は「2つの要素が保持するプロパティの同一性」を調べる関数です。要するにdata classなら==で比較するだけです。

ちなみに、RecyclerView登場以後〜ListAdapter登場前のアダプタークラスの要素比較処理は `DiffUtil.Callback` クラスを使って実装するのが一般的でしたが、 `ListAdapter` と `DiffUtil.**Item**Callback` の登場によって見かける機会は激減しました。
`DiffUtil.Callback` ではユーザーが変更前後の要素リストを見比べて変更状態を自前で実装しなければならなかったのに対して、 `DiffUtil.**Item**Callback` では単純に要素同士の同一性を実装するだけでよいので、やはりここでも手間が激減しています。

### ListAdapterとViewHolder

DataBindingを使用する場合の `ViewHolder` はBindingクラスをプロパティとして持たせてやるようにし、ViewHolderの操作はBindingクラスを主体に考えます。

```kotlin
    class UserViewHolder(private val binding: UserViewBinding) :
        RecyclerView.ViewHolder(binding.root)
```

`UserListAdapter#onCreateViewHolder` では、素直にUserViewHolderを作ってやればOKです。

```kotlin
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return UserViewHolder(UserViewBinding.inflate(layoutInflater, parent, false))
    }
```

`UserListAdapter#onBindViewHolder` では、正味の処理は `UserViewHolder` に委譲してしまいます。具体的には `UserViewHolder#bind` 関数を呼ぶだけです。

```kotlin
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position), viewLifecycleOwner, viewModel)
    }
```

もう一度 `UserViewHolder` に戻り、bind関数の中身を見てみましょう。

```kotlin
        fun bind(item: User, viewLifecycleOwner: LifecycleOwner, viewModel: MainViewModel) {
            binding.run {
                lifecycleOwner = viewLifecycleOwner
                user = item
                this.viewModel = viewModel

                executePendingBindings()
            }
        }
```

いつもActivityクラスやFragmentクラスでやるように、bindingオブジェクトに対して値をセットしてやります。

最後に `executePendingBindings()` を呼ぶのを忘れないでください。**executePendingBindings()がないとbind関数が呼ばれても画面反映が即座に行われません**。筆者はよく忘れます。

なお、今回のリストUIは、

* UI（スイッチ）のON/OFF操作とUser#isCheckedの反映を即座に行いたい -> LiveDataをbindingするためlifecycleOwnerが必要
* リスト要素のタップイベントを取得したい -> viewModelが必要

という要件があるので、リスト要素と直接ひも付く `item: User` のほかに、`viewLifecycleOwner: LifecycleOwner` 、 `viewModel: MainViewModel` が引数として登場することになり、bindingにセットしています。

```kotlin
class UserListAdapter(
    private val viewLifecycleOwner: LifecycleOwner,
    private val viewModel: MainViewModel
) : ListAdapter<User, UserListAdapter.UserViewHolder>(DiffCallback) {
```

`UserListAdapter` のコンストラクタ引数に `viewLifecycleOwner` と `viewModel` を指定したのは巡り巡って `UserViewHolder#bind` で使用するためです。
逆にこれらの要件がない場合、`UserListAdapter` のコンストラクタ引数にこれらの値は必要ありません。

* リスト要素は操作UIを持たない（＝エンティティクラスのプロパティの値を書き換えない、または即時反映不要） -> lifecycleOwnerは不要
* リスト要素のタップイベントは不要 -> viewModelは不要

ListAdapterのクラスを新造する場合、既存のクラスをコピペして作りがちですが、引数のLifecycleOwnerやViewModelは必要に応じて削りましょう。

## Fragmentクラス `MainFragment`

最後にFragmentクラスです。

```MainFragment.kt
class MainFragment : Fragment() {

    ...

    private val viewModel: MainViewModel by viewModels()
    private lateinit var userListAdapter: UserListAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return MainFragmentBinding.inflate(inflater, container, false)
            .apply {

                ...

                list.run {
                    layoutManager = LinearLayoutManager(context)
                    addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))

                    adapter = UserListAdapter(viewLifecycleOwner, this@MainFragment.viewModel).also {
                        userListAdapter = it
                    }

                }
            }
            .run {
                root
            }
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        viewModel.run {
            users.observe(viewLifecycleOwner, {
                userListAdapter.submitList(it)
            })

            ...
        }
    }

}
```

といっても `UserListAdapter` ほどの驚きはありません。

`onCreateView` では `MainFragmentBinding` を生成し、 `viewLifecycleOwner` 、 `viewModel` をセットします。
`list`（RecyclerView）にはいつもどおり `LayoutManager` 、`ItemDecoration` をセットしてから `UserListAdapter` をセットしてやります。
ちなみに、 **`LayoutManager` のセットを忘れるといくら `UserListAdapter` に値を渡しても描画が行われません** 。筆者はよくやらかします。

`onActivityCreated` では `MainViewModel` の `users` をobserveします。
もちろん、observeされた時の処理は `UserListAdapter` への通知です。
`ListAdapter` には `submitList` という関数が用意されているのでこれを呼び出します。前述のとおり、 `notifyDataSetChanged` も `notifyItemXXX` も呼び出す必要はありません。

# 完全なサンプル

https://github.com/quwac/howtouserecyclerview2020

# 終わりに

メモのつもりだったのに思いのほか長くなってしまった。。。
途中かなり端折ったので意味不明になっている箇所があればコメントをお願いします。
もちろん「そのやり方間違っているよ」も歓迎です。

# 2021/02/13追記

続編記事を書きました。

[【続】2020年版RecyclerViewの使い方 〜リストのアイテムに複数のレイアウトを使う〜](https://qiita.com/que9/items/59cc988bfd92d0acea07)