【続】2020年版RecyclerViewの使い方 〜リストのアイテムに複数のレイアウトを使う〜

# tl;dr

* [2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel](https://qiita.com/que9/items/52725c3caf33f2a9b44e) で紹介しなかった、リストのアイテムに複数のレイアウトを使う場合を紹介
* ViewHolderの抽象クラス化が鍵
* `getItemViewType` のoverrideを忘れるな
* ソースコード
    * 今回のサンプルのソースコード：https://github.com/quwac/how-to-use-recyclerview-2020/tree/multiple_view_type
    * 前回のソースコードとの差分表示：https://github.com/quwac/how-to-use-recyclerview-2020/compare/main..multiple_view_type

# はじめに

[2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel](https://qiita.com/que9/items/52725c3caf33f2a9b44e) のコメント欄で、リストのアイテムのレイアウトがそれぞれ異なる場合の書き方について質問をいただきました（ありがとうございます！）
コメント欄で回答させていただいたのですが、せっかくなので記事としても残しておこうと思います。

# 前置き

[2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel](https://qiita.com/que9/items/52725c3caf33f2a9b44e) で作ったコードの改修という形で説明します。
先にそちらの記事を読んでおいてください。

コードは `-` が削除行、 `+` が追加行を表します。

# 作るもの

以前作ったアプリではリストのアイテムそれぞれにスイッチUIが付いていました。

今回は、アイテムによってスイッチUIが付いているもの（表示されているもの）と、付いていないもの（非表示のもの）があるUIを作ります。
また、そのUIはそれぞれ別々のレイアウトファイルで実現することにします[^1]。

[^1]: 実際のケースでは、スイッチUIの有無程度の違いなら、レイアウトファイルを分けるよりスイッチUIのvisibilityにデータバインディングして表示/非表示を制御した方が良いです。今回は例ということでご勘弁ください

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

```diff:User.kt
+ enum class UserType {
+     USER_SWITCHABLE,
+     USER_UNSWITCHABLE,
+ }

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

```

はじめに、`User` クラスにスイッチUIを表示するか（USER_SWITCHABLE）、非表示にするか（USER_UNSWITCHABLE）を表す値をもたせるようにします。
booleanで持たせても構いませんが、今回はenumを使うことにします。

`User` クラスの変更にともない `MainViewModel` クラスにも変更が生じていますが、本質的な話題ではないのでそちらは割愛します。

## レイアウトファイル `user_view_switchable.xml` 、 `user_view_unswitchable.xml`

続いてレイアウトファイルです。

`user_view_switchable.xml` はスイッチUIが表示されている方のレイアウトファイルです。
改修前の `user_view.xml` をリネームしただけのファイルです。

`user_view_unswitchable.xml` はスイッチUIが非表示の方のレイアウトファイルです。
`user_view_switchable.xml` からコピペした後、スイッチUIを消してconstraintを少しいじったファイルです。

```user_view_unswitchable.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" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/selectableItemBackground"
        android:padding="16dp">

        <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_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

`user_view_switchable.xml` にはあった、 `<variable name="viewModel" ... ` タグがなくなっていることに注目してください。
スイッチUIが消えたのでクリックイベントとバインディングする必要がなくなりました。
それに応じて `<variable name="viewModel" ... ` タグも削除しています。
これは、後に示すバインディングの処理で影響してきます。

## ViewHolderクラス `UserViewHolder` 、 `UserViewSwitchableHolder` 、 `UserViewUnswitchableHolder`

`UserListAdapter` クラスの前に `UserViewHolder` クラスを見ていきましょう。

改修前の `UserViewHolder` クラスは、その名のとおり `UserViewBinding` クラスのインスタンスを保持する役割のクラスでした。
今回の改修で `UserViewBinding` はなくなり、代わりに `UserViewSwitchableBinding` と `UserViewUnswitchableBinding` の2つを扱うことになりました。

さてそのための改修ですが、残念ながら、 `UserViewHolder` だけではこれら2つのインスタンスを保持するようなクラスは作れません。
代わりに、それぞれを保持する専用のViewHolderクラス `UserViewSwitchableHolder` 、 `UserViewUnswitchableHolder` を作ります。
また、 `UserViewSwitchableHolder` と `UserViewUnswitchableHolder` は `UserViewHolder` を親とする継承クラスとすることでコードをすっきりさせます。

はじめに、 `UserViewHolder` を改修します。

```diff
-     class UserViewHolder(binding: UserViewBinding) :
+     abstract class UserViewHolder(binding: ViewDataBinding) :
        RecyclerView.ViewHolder(binding.root) {


-         fun bind(item: User, viewLifecycleOwner: LifecycleOwner, viewModel: MainViewModel) {
-         ...
-         }
+         abstract fun bind(item: User, viewLifecycleOwner: LifecycleOwner, viewModel: MainViewModel)
      }

```

bind関数の実装は子クラスに委ねるようにします（理由は後述）。そのため、bind関数を抽象関数化（abstractを付与）して実装を消しています。
これに伴いクラスも抽象クラス化しなければならないためabstractを付与します。

コンストラクタ引数の型は　`UserViewBinding` から `ViewDataBinding` に変更します。
`ViewDataBinding` は全ての `〜Binding` クラスの親クラスです。
ここを `ViewDataBinding` にしておくことで、 `UserViewHolder` の子クラスからは任意の `〜Binding` クラスインスタンスを受け取ることができます。

つぎに、 `UserViewHolder` を継承する `UserViewSwitchableHolder` と `UserViewUnswitchableHolder` を作ります。

```kotlin
    class UserViewSwitchableHolder(private val binding: UserViewSwitchableBinding) :
        UserViewHolder(binding) {
        override fun bind(
            item: User,
            viewLifecycleOwner: LifecycleOwner,
            viewModel: MainViewModel
        ) {
            binding.run {
                lifecycleOwner = viewLifecycleOwner
                user = item
                this.viewModel = viewModel

                executePendingBindings()
            }
        }
    }

    class UserViewUnswitchableHolder(private val binding: UserViewUnswitchableBinding) :
        UserViewHolder(binding) {
        override fun bind(
            item: User,
            viewLifecycleOwner: LifecycleOwner,
            viewModel: MainViewModel
        ) {
            binding.run {
                user = item

                executePendingBindings()
            }
        }
    }
```

`UserViewHolder` で抽象化した `bind` 関数を、それぞれのクラスで実装しています。
2つのクラスのbind関数の実装を見比べてください。
`UserViewSwitchableHolder` では `this.viewModel = viewModel` と `lifecyleOwner = viewLifecycleOwner` の文がありますが、 `UserViewUnswitchableHolder` ではこれらの文はありません。
レイアウトファイルの節で説明したとおり、 `UserViewUnswitchableBinding` では `viewModel` とのバインディングが必要なくなったので、 `this.viewModel = viewModel` がなくなりました。
加えて、LiveData型の変数とのバインディングがなくなりライフサイクルを知る必要もなくなったため、 `lifecycleOwner = viewLifecycleOwner` もなくなりました。
このように、bind関数を抽象化し子クラスで実装を委ねているのは、レイアウトファイルごとにバインディング処理が異なることがあるためです。
下手に共通実装とするとドツボにハマるため、バインディング処理は子クラスごとに分けるのが基本形だと覚えておいてください。

コンストラクタにおいては、 `UserViewSwitchableHolder` は `UserViewSwitchableBinding` を、 `UserViewUnswitchableHolder` は `UserViewUnswitchableBinding` を引数として取ります。
`UserViewHolder` のコンストラクタ引数の型は先程 `ViewDataBinding` に変更しておいたので、 `: UserViewHolder(binding)` でどちらの型も受け取ることができます。

## ListAdapterクラス `UserListAdapter`

メインの改修は `onCreateViewHolder` 関数になります。
`onCreateViewHolder` ではリストのアイテムと対応する `UserViewHolder` 型のインスタンスを生成し、戻り値として返しています。
今回の改修では先程作成した `UserViewSwitchableHolder` クラスのインスタンスまたは `UserViewUnswitchableHolder` クラスのインスタンスをいい感じ作り分けをしてやるようにします。
どちらのクラスのインスタンスを作ればいいかは、関数の引数の `viewType` の値によって決めるようにします。
では、 `viewType` の値はどのような値が設定されるのでしょうか？

答えは、ListAdapterクラス（の継承元のRecyclerView.Adapterクラス）で定義されている `getItemViewType` という関数の戻り値が設定されます。
`getItemViewType` は普段は0しか返さない関数なので、今回のように複数のレイアウトファイルを使う場合はこの関数のoverrideして所望の値を返すよう実装してやる必要があります。
このoverrideが忘れがちなので注意してください。

```UserListAdapter.kt
    override fun getItemViewType(position: Int): Int {
        return getItem(position).userType.ordinal
    }
```

`viewType` はint型なので、スイッチUIを表示するか非表示にするかもint型として返してやる必要があります。
ここで `UserType` をenumにしたことが活きてきます。
enumにはordinalという要素と1対1対応するint値を返すプロパティが存在しているので、この値をそのまま返してやるようにします。楽ちんですね。

最後に、 `onCreateViewHolder` 関数です。

```diff:UserListAdapter.kt
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)


-       return UserViewHolder(UserViewBinding.inflate(layoutInflater, parent, false))
+       val type = UserType.values()[viewType]
+       return when (type) {
+           UserType.USER_SWITCHABLE -> UserViewSwitchableHolder(
+               UserViewSwitchableBinding.inflate(
+                   layoutInflater,
+                   parent,
+                   false
+               )
+           )
+           UserType.USER_UNSWITCHABLE -> UserViewUnswitchableHolder(
+               UserViewUnswitchableBinding.inflate(
+                   layoutInflater,
+                   parent,
+                   false
+               )
+           )
+       }
+   }
```

前述のとおり、 `viewType` の値によって `UserViewSwitchableHolder` と `UserViewUnswitchableHolder` の作り分けをしています。
`viewType` には `UserType` のordinalの値が設定されているので、 `val type = UserType.values()[viewType]` でordinalの値から `UserType` を復元します。
あとは復元された `UserType` の値で分岐してやればいいだけです！

# 完全なサンプル

* 今回のサンプルのソースコード：https://github.com/quwac/how-to-use-recyclerview-2020/tree/multiple_view_type
* 改修前のソースコードとの差分表示：https://github.com/quwac/how-to-use-recyclerview-2020/compare/main..multiple_view_type

# 終わりに

前回の記事を書いてから年が明けてしまいましたが、2021年2月時点ではまだこのやり方が通用するはずです。
良きAndroidライフを！👍

# 追記

enum <-> int変換で誤解があるといやなので番外編記事を書きました。

[enumのDB保存にordinalを使ってはだめ](https://qiita.com/que9/items/2f958ba84a65e55caa12)