［Android］RecyclerView の ConcatAdapter を使う方法

# はじめに

例えば今までの RecyclerView.Adapter を利用した実装では "ヘッダーを表示する View" と "コンテンツを表示する View" を組み合わせて表示するとなると大変だったんですが ConcatAdapter を利用すると簡単に実装できるらしいです。

[![Image from Gyazo](https://i.gyazo.com/d24ad24ca8152527ea81e734a847270a.jpg)](https://gyazo.com/d24ad24ca8152527ea81e734a847270a)

ConcatAdapter を利用して "ヘッダーを表示する View" と "コンテンツを表示する View" を組み合わせた表示どうやったらできるのか以下のサンプルを作って調べたのでまとめたいと思います。

![recycler_view.gif](https://i.gyazo.com/4cda6fb39f814a6aa57975b9318f6e3c.gif)

# セットアップ

ConcatAdapter は Version 1.2.0-alpha03 から MergerAdapter という名称で追加されたあと、Version 1.2.0-alpha04 にて今の ConcatAdapter という名称に変更されました。なので ConcatAdapter を使うには Version 1.2.0-alpha04 以上のバージョンを使う必要があります。今回は現時点での最新版である Version 1.2.0-rc01 を使っていこうかと思います。

```groovy
dependencies {
    ︙
    implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
    ︙
}
```

# 表示するデータを宣言する

User という firstName と lastName, age を持つデータクラスと Category という title を持つデータクラスを定義します。今回はこの User と Category に定義したデータを ConcatAdapter を利用して一つの RecyclerView で表示できるようにします。

```kotlin
data class User(val firstName: String, val lastName: String, val age: Int) {
    val id = UUID.randomUUID().toString()
}
```

```kotlin
data class Category(val title: String) {
    val id = UUID.randomUUID().toString()
}
```

# 表示する View を宣言する

今回は viewBinding を利用したいので buildFeatures で viewBinding を true にしておきます。

```groovy
android {
    ︙省略
    buildFeatures {
        viewBinding true
    }
    ︙省略
}
```

Category を表示する View を宣言します。layout_category_item.xml というファイルに以下の定義をして View として title を表示できるようにしておきます。

[![Image from Gyazo](https://i.gyazo.com/29612ca276bbbe88a0ab70a0d9aa68cc.png)](https://gyazo.com/29612ca276bbbe88a0ab70a0d9aa68cc)

```xml
<?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 を表示できるようにしておきます。

![clipboard.png](https://i.gyazo.com/4534f61d74c864a8d0f18b99dd369451.png)

```xml
<?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 を定義します。

```kotlin
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))
    }
}
```

```kotlin
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です。

1. ConcatAdapter.add にて ReyclerView.Adapter を追加する
2. ConcatAdapter.add で追加した RecyclerView.Adapter の submitList を呼び出す

```kotlin
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
<?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>
```

[![Image from Gyazo](https://i.gyazo.com/4cda6fb39f814a6aa57975b9318f6e3c.gif)](https://gyazo.com/4cda6fb39f814a6aa57975b9318f6e3c)

どうやら ConcatAdapter.add を実行した順で表示されるようです。例えば次のように UserListAdapter を追加した後に CategoryAdapter を追加すると順番が逆になります。

```kotlin
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)
            )
        )
    }
}
```

[![Image from Gyazo](https://i.gyazo.com/7470ccd25ff25302c90abb0d8dbaf26c.gif)](https://gyazo.com/7470ccd25ff25302c90abb0d8dbaf26c)
# おわりに

というように ConcatAdapter を使うと簡単にヘッダー付きやフッダー付きの RecyclerView を作成できます。以前はヘッダーやフッダーをつけようとすると RecyclerView.Adapter が複雑化してしまうことがありましたがこれなら簡単にできるかなと思います。

しかし ConcatAdapter は RecyclerView.Adapter を結合するだけなので複雑な表示をしたいならば Epoxy や Groupie を使うのが良さそうだと思いました。まだまだ公式が提供する Adapter だけでは実装が難しい表示があるのは変わらないですね。

https://developer.android.com/jetpack/androidx/releases/recyclerview