- 投稿日:2021-02-13T18:48:17+09:00
【続】2020年版RecyclerViewの使い方 〜リストのアイテムに複数のレイアウトを使う〜
tl;dr
- 2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel で紹介しなかった、リストのアイテムに複数のレイアウトを使う場合を紹介
- ViewHolderの抽象クラス化が鍵
getItemViewType
のoverrideを忘れるな- ソースコード
はじめに
2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel のコメント欄で、リストのアイテムのレイアウトがそれぞれ異なる場合の書き方について質問をいただきました(ありがとうございます!)
コメント欄で回答させていただいたのですが、せっかくなので記事としても残しておこうと思います。前置き
2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel で作ったコードの改修という形で説明します。
先にそちらの記事を読んでおいてください。コードは
-
が削除行、+
が追加行を表します。作るもの
以前作ったアプリではリストのアイテムそれぞれにスイッチUIが付いていました。
今回は、アイテムによってスイッチUIが付いているもの(表示されているもの)と、付いていないもの(非表示のもの)があるUIを作ります。
また、そのUIはそれぞれ別々のレイアウトファイルで実現することにします1。エンティティクラス
User
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
を改修します。- 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
を作ります。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.ktoverride fun getItemViewType(position: Int): Int { return getItem(position).userType.ordinal }
viewType
はint型なので、スイッチUIを表示するか非表示にするかもint型として返してやる必要があります。
ここでUserType
をenumにしたことが活きてきます。
enumにはordinalという要素と1対1対応するint値を返すプロパティが存在しているので、この値をそのまま返してやるようにします。楽ちんですね。最後に、
onCreateViewHolder
関数です。UserListAdapter.ktoverride 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ライフを!?
実際のケースでは、スイッチUIの有無程度の違いなら、レイアウトファイルを分けるよりスイッチUIのvisibilityにデータバインディングして表示/非表示を制御した方が良いです。今回は例ということでご勘弁ください ↩