20210213のAndroidに関する記事は1件です。

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

tl;dr

はじめに

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.xmluser_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クラス UserViewHolderUserViewSwitchableHolderUserViewUnswitchableHolder

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

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

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

はじめに、 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 を継承する UserViewSwitchableHolderUserViewUnswitchableHolder を作ります。

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

コンストラクタにおいては、 UserViewSwitchableHolderUserViewSwitchableBinding を、 UserViewUnswitchableHolderUserViewUnswitchableBinding を引数として取ります。
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 関数です。

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 の値によって UserViewSwitchableHolderUserViewUnswitchableHolder の作り分けをしています。
viewType には UserType のordinalの値が設定されているので、 val type = UserType.values()[viewType] でordinalの値から UserType を復元します。
あとは復元された UserType の値で分岐してやればいいだけです!

完全なサンプル

終わりに

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


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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む