【Android】MergeAdapterを使ってBanner + GridItemのパターンを実装する

## 概要

巷によくあるアプリのパターンとして、画面の上に横長のバナーやレイアウトを置いて、その下に格子状のレイアウトを置くというものがあります。


SmartNews
<img width="351" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99605/a8a385b5-3ab0-1b88-cd51-cc711a9f7eaf.png">

グノシー
<img width="351" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99605/6241b30e-6def-ab26-55ec-4ba99bfacda8.png">

このデザインに対応するためのよくある実装として、一つのRecylerAdapterで、ViewHolderを複数用意して、item毎にViewHolderを切り替えるといったものがあります。
しかし、ロジックが複雑化するので、実装コストや後のメンテナンスコストが増えてしまう原因でもあります。


今回はよくあるこのパターンを、recyclerviewのalpha版に実装されているMergeAdapterを使って実装していきます。

## Sample

[https://github.com/alpha2048/MergeAdapterTest](https://github.com/alpha2048/MergeAdapterTest)

<img width="298" alt="スクリーンショット 2020-06-21 17.00.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99605/b4400088-f975-f43d-09f5-a7e3698f31ee.png">


## 解説

MergeAdapterはrecyclerviewの1.2.0のalpha02から入っています。

```
    implementation "androidx.recyclerview:recyclerview:1.2.0-alpha03"
```

実装はとても簡単です

1. Banner用のAdapterを作成
2. GridItem用のAdapterを作成
3. MergeAdapterに上の2つをマージする

Banner用のAdapter

```
class BannerAdapter() : RecyclerView.Adapter<ItemBannerViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemBannerViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = ItemListBannerBinding.inflate(layoutInflater, parent, false)
        return ItemBannerViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ItemBannerViewHolder, position: Int) {
    }

    override fun getItemCount(): Int {
        return 1
    }
}

class ItemBannerViewHolder(val binding: ItemListBannerBinding) : RecyclerView.ViewHolder(binding.root)
```

GridItem用のAdapter

```
class GridItemAdapter (private val viewModel: MainViewModel) : RecyclerView.Adapter<ItemGridViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemGridViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = ItemListGridItemBinding.inflate(layoutInflater, parent, false)
        return ItemGridViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ItemGridViewHolder, position: Int) {
        holder.binding.image.load(viewModel.repoItems[position].owner.avatar_url)
    }

    override fun getItemCount(): Int {
        return viewModel.repoItems.size
    }

}

class ItemGridViewHolder(val binding: ItemListGridItemBinding) : RecyclerView.ViewHolder(binding.root)
```

この2つをMergeAdapterにマージする

```
        val bannerAdapter = BannerAdapter()
        val gridItemAdapter = GridItemAdapter(viewModel)

        val mergeAdapter = MergeAdapter(
            bannerAdapter,
            gridItemAdapter
        )

        binding.recyclerView.apply {
            val gridLayoutManager = GridLayoutManager(context,3)
            gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int = if (position == 0) 3 else 1
            }
            layoutManager = gridLayoutManager
            adapter = mergeAdapter
        }
```

注意点として、LayoutManagerはGridLayoutManagerを使うので、バナーのように1列に表示する場合はSpanSizeの制御をきちんと入れましょう。

## あとがき

簡単な実装で、性質の違うデータ、レイアウトを別々のAdapterで管理できるのはとてもよいですね！
地味にいろんなところで使えそうなので、安定版のリリースが待ち遠しいです。

## 参考資料

- [MergeAdapter(Google公式)](https://developer.android.com/reference/androidx/recyclerview/widget/MergeAdapter?hl=ja)