- 投稿日: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。エンティティクラス
UserUser.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にデータバインディングして表示/非表示を制御した方が良いです。今回は例ということでご勘弁ください ↩
