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
+ 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を少しいじったファイルです。
<?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が忘れがちなので注意してください。
override fun getItemViewType(position: Int): Int {
return getItem(position).userType.ordinal
}
viewType
はint型なので、スイッチUIを表示するか非表示にするかもint型として返してやる必要があります。
ここで UserType
をenumにしたことが活きてきます。
enumにはordinalという要素と1対1対応するint値を返すプロパティが存在しているので、この値をそのまま返してやるようにします。楽ちんですね。
最後に、 onCreateViewHolder
関数です。
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変換で誤解があるといやなので番外編記事を書きました。
-
実際のケースでは、スイッチUIの有無程度の違いなら、レイアウトファイルを分けるよりスイッチUIのvisibilityにデータバインディングして表示/非表示を制御した方が良いです。今回は例ということでご勘弁ください ↩