はじめに
例えば今までの RecyclerView.Adapter を利用した実装では "ヘッダーを表示する View" と "コンテンツを表示する View" を組み合わせて表示するとなると大変だったんですが ConcatAdapter を利用すると簡単に実装できるらしいです。
ConcatAdapter を利用して "ヘッダーを表示する View" と "コンテンツを表示する View" を組み合わせた表示どうやったらできるのか以下のサンプルを作って調べたのでまとめたいと思います。
セットアップ
ConcatAdapter は Version 1.2.0-alpha03 から MergerAdapter という名称で追加されたあと、Version 1.2.0-alpha04 にて今の ConcatAdapter という名称に変更されました。なので ConcatAdapter を使うには Version 1.2.0-alpha04 以上のバージョンを使う必要があります。今回は現時点での最新版である Version 1.2.0-rc01 を使っていこうかと思います。
dependencies {
︙
implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
︙
}
表示するデータを宣言する
User という firstName と lastName, age を持つデータクラスと Category という title を持つデータクラスを定義します。今回はこの User と Category に定義したデータを ConcatAdapter を利用して一つの RecyclerView で表示できるようにします。
data class User(val firstName: String, val lastName: String, val age: Int) {
val id = UUID.randomUUID().toString()
}
data class Category(val title: String) {
val id = UUID.randomUUID().toString()
}
表示する View を宣言する
今回は viewBinding を利用したいので buildFeatures で viewBinding を true にしておきます。
android {
︙省略
buildFeatures {
viewBinding true
}
︙省略
}
Category を表示する View を宣言します。layout_category_item.xml というファイルに以下の定義をして View として title を表示できるようにしておきます。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/category_title_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="48sp"
tools:text="CATEGORY" />
</FrameLayout>
User を表示する View を宣言しておきます。layout_user_item.xml というファイルに以下の定義をして定義して firstName や lastName, age を表示できるようにしておきます。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="#eeeeee"
android:padding="8dp">
<TextView
android:id="@+id/age_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="64sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/first_name_text_view"
app:layout_constraintTop_toTopOf="parent"
tools:text="100" />
<TextView
android:id="@+id/first_name_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="32sp"
app:layout_constraintBottom_toTopOf="@id/last_name_text_view"
app:layout_constraintEnd_toStartOf="@id/age_text_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="FIRST NAME" />
<TextView
android:id="@+id/last_name_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/age_text_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/first_name_text_view"
tools:text="LAST NAME" />
</androidx.constraintlayout.widget.ConstraintLayout>
表示を制御するアダプターを作成する
ConcatAdapter に追加する RecyclerView.Adapter を作成します。Category を表示する CategoryAdapter、User を表示する UserListAdapter の 2つの RecyclerView.Adapter を定義します。
class CategoryItemViewHolder(
private val binding: LayoutCategoryItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(category: Category) {
binding.categoryTitleTextView.text = category.title
}
}
val DIFF_UTIL_CATEGORY_ITEM_CALLBACK = object : DiffUtil.ItemCallback<Category>() {
override fun areContentsTheSame(oldItem: Category, newItem: Category): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(oldItem: Category, newItem: Category): Boolean {
return oldItem.id == newItem.id
}
}
class CategoryAdapter : ListAdapter<Category, CategoryItemViewHolder>(DIFF_UTIL_CATEGORY_ITEM_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryItemViewHolder {
val view = LayoutCategoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CategoryItemViewHolder(view)
}
override fun onBindViewHolder(holder: CategoryItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
class UserItemViewHolder(
private val binding: LayoutUserItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(user: User) {
binding.firstNameTextView.text = user.firstName
binding.lastNameTextView.text = user.lastName
binding.ageTextView.text = user.age.toString()
}
}
val DIFF_UTIL_USER_ITEM_CALLBACK = object : DiffUtil.ItemCallback<User>() {
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
}
class UserListAdapter : ListAdapter<User, UserItemViewHolder>(DIFF_UTIL_USER_ITEM_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserItemViewHolder {
val view = LayoutUserItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return UserItemViewHolder(view)
}
override fun onBindViewHolder(holderUser: UserItemViewHolder, position: Int) {
holderUser.bind(getItem(position))
}
}
ConcatAdapter を使ってデータを表示する
あとは RecyclerView を定義して、作成した RecyclerView.Adapter を ConcatAdapter にセットすればOKです。
- ConcatAdapter.add にて ReyclerView.Adapter を追加する
- ConcatAdapter.add で追加した RecyclerView.Adapter の submitList を呼び出す
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupConcatAdapter()
val categoryAdapter = CategoryAdapter()
val userListAdapter = UserListAdapter()
val concatAdapter = ConcatAdapter().apply {
addAdapter(categoryAdapter)
addAdapter(userListAdapter)
}
binding.recyclerView.adapter = concatAdapter
binding.recyclerView.layoutManager =
LinearLayoutManager(applicationContext, RecyclerView.VERTICAL, false)
categoryAdapter.submitList(listOf(Category("人名図鑑")))
userListAdapter.submitList(
listOf(
User("あいざわ", "かずき", 29),
User("ふじくら", "まさひろ", 52),
User("よしずみ", "ひろゆき", 54),
User("ほりのうち", "しんいち", 40),
User("はすぬま", "よしろう", 37),
User("はなわ", "のぶお", 38),
User("おじま", "おじま", 31),
User("しんざき", "くにひと", 35)
)
)
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
どうやら ConcatAdapter.add を実行した順で表示されるようです。例えば次のように UserListAdapter を追加した後に CategoryAdapter を追加すると順番が逆になります。
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupConcatAdapter()
val categoryAdapter = CategoryAdapter()
val userListAdapter = UserListAdapter()
val concatAdapter = ConcatAdapter().apply {
addAdapter(userListAdapter)
addAdapter(categoryAdapter)
}
binding.recyclerView.adapter = concatAdapter
binding.recyclerView.layoutManager =
LinearLayoutManager(applicationContext, RecyclerView.VERTICAL, false)
categoryAdapter.submitList(listOf(Category("人名図鑑")))
userListAdapter.submitList(
listOf(
User("あいざわ", "かずき", 29),
User("ふじくら", "まさひろ", 52),
User("よしずみ", "ひろゆき", 54),
User("ほりのうち", "しんいち", 40),
User("はすぬま", "よしろう", 37),
User("はなわ", "のぶお", 38),
User("おじま", "おじま", 31),
User("しんざき", "くにひと", 35)
)
)
}
}
おわりに
というように ConcatAdapter を使うと簡単にヘッダー付きやフッダー付きの RecyclerView を作成できます。以前はヘッダーやフッダーをつけようとすると RecyclerView.Adapter が複雑化してしまうことがありましたがこれなら簡単にできるかなと思います。
しかし ConcatAdapter は RecyclerView.Adapter を結合するだけなので複雑な表示をしたいならば Epoxy や Groupie を使うのが良さそうだと思いました。まだまだ公式が提供する Adapter だけでは実装が難しい表示があるのは変わらないですね。