LoginSignup
11
3

More than 3 years have passed since last update.

MergeAdapterを試してみた

Last updated at Posted at 2020-04-25

これまでRecyclerViewで異なるレイアウトのコンポーネントを組み合わせる場合、共通のAdapter内でViewHolderを複数作成し、viewTypeで処理を分けるのが一般的だったと思います。

その結果、Adapterが肥大化して可読性が悪くなってしまう経験をした方は多いのではないでしょうか。

そんなあなたへの朗報、MergeAdapterを使うことでAdapterを分離し、スッキリと書くことができるようになりました。

この記事では、このMergeAdapterについて簡単な実装からちょっとした疑問点へのお答えまでできればと思います。

  • 実装方法
  • 1つのAdapterのみ更新したらどうなるのか
  • viewTypeは指定しなくて良いのか

MergeAdapterとは

ドキュメントはこちら

An Adapter implementation that presents the contents of multiple adapters in sequence

複数のadapterを順番に、1つのadapterにまとめられるよ~という感じです。

val adapter1: MyAdapter = ...
val adapter2: AnotherAdapter = ...
val merged = MergeAdapter(adapter1, adapter2)
recyclerView.adapter = merged

実装方法

どんなのを実装するか

以下のように、3つのセクションで構成されるRecyclerViewを例に実装方法を紹介します。

  • TextViewを持つコンポーネント x N個
  • Buttonを持つコンポーネント x 1個
  • Switchを持つコンポーネント x N個

なお、今回作ったサンプルコードはこちら

image.png

recyclerview:1.2.0-alpha02を使う

このMergeAdapterは recyclerview:1.2.0-alpha02 以降で使えるため、これ以降のバージョンを指定します。

implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'

セクションごとにAdapterを定義する

まず、それぞれのセクションのAdapterを実装します

以下は、TextViewの部分のAdapterです

class TextSectionAdapter: ListAdapter<TextItem, TextSectionViewHolder>(DIFF_CALLBACK) {

    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<TextItem>() {
            override fun areContentsTheSame(
                oldItem: TextItem,
                newItem: TextItem
            ): Boolean {
                android.util.Log.d("TextSectionAdapter", "onChanged")
                return oldItem == newItem
            }

            override fun areItemsTheSame(
                oldItem: TextItem,
                newItem: TextItem
            ) = oldItem.id == newItem.id
        }
    }

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

    override fun onBindViewHolder(holder: TextSectionViewHolder, position: Int) {
        holder.bind(getItem(position))
    }


}

class TextSectionViewHolder(private val binding: ItemSectionTextBinding): RecyclerView.ViewHolder(binding.root) {
    fun bind(item: TextItem) {
        binding.textView.text = item.text
    }
}

data class TextItem(val id: Int, val text: String)

この1つのレイアウトに関する操作だけで済むため、Adapterがスッキリしてるかと思います。
また同様の理由で、getItemViewTypeもオーバーライドしてません。

同じようにして、他のセクション用のAdapterも定義します。

各AdapterをMergeしてRecyclerViewにセットする

MergeAdapterを使って各AdapterをMergeしてRecyclerViewにセットして準備OKです。
なお、MergeAdapterの引数に入れた順番にadapterが結合されます。
UIの表示順を変えたい時には、ここで引数に入れる順番を変えてください。

        val textSectionAdapter = TextSectionAdapter()
        val switchSectionAdapter = SwitchSectionAdapter()
        val buttonSectionAdapter = ButtonSectionAdapter()

        val mergeAdapter = MergeAdapter(
            textSectionAdapter,
            buttonSectionAdapter,
            switchSectionAdapter
        )

        recyclerView.apply {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(context)
            adapter = mergeAdapter
        }

各Adapterのアイテムを更新する

今回は、TextのセクションとSwitchのセクションに、それぞれ15個の値をlistにして流し込んでいます。

        val textItems = arrayListOf<TextItem>()
        val switchItems = arrayListOf<SwitchItem>()

        (1..15).forEach {
            textItems.add(TextItem(id = it, text = it.toString()))
            switchItems.add(SwitchItem(id = it, isChecked = it%2==0))
        }

        textSectionAdapter.submitList(textItems)
        switchSectionAdapter.submitList(switchItems)

これで、初期表示で以下のような画面を表示することができます。

image.png

1つのAdapterのみ更新したらどうなるのか

ここで疑問に感じるのが、もし一部だけのadapterのアイテムを更新したらどうなるの??ということです。
この一部のadapterにだけ更新が通知されるのか、あるいはmergeされた全てのadapterに通知されるのか。

結論から言うと、更新をかけたadapterのみで更新が走り、他のadapterには影響はないようです。

これを検証するため、TOGGLE BUTTONを押下したら、Switchセクションのみ全てのswitch状態を反転させるようにしてみました。


            val buttonSectionAdapter = ButtonSectionAdapter() {
            android.util.Log.d("buttonSectionAdapter", "onClicked")

            // Switchセクションのアイテムを更新
            switchSectionAdapter.submitList(switchSectionAdapter.currentList.map { it.copy( isChecked = !it.isChecked) })

            }


class ButtonSectionAdapter(private val onClick: () -> Unit): RecyclerView.Adapter<ButtonSectionViewHolder>() {

...

}

同時に、SwitchASectiondapterとTextSectionAdapterのDiffUtilコールバックの箇所にもログを入れます。


private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<SwitchItem>() {

            override fun areContentsTheSame(
                oldItem: SwitchItem,
                newItem: SwitchItem
            ): Boolean {
                android.util.Log.d("SwitchSectionAdapter", "ItemCallback")
                return oldItem == newItem
            }
...

}


  private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<TextItem>() {
            override fun areContentsTheSame(
                oldItem: TextItem,
                newItem: TextItem
            ): Boolean {
                android.util.Log.d("TextSectionAdapter", "ItemCallback")
                return oldItem == newItem
            }
...

}

この状態でTOGGLE BUTTONを押下するとログは以下のようになり、SwitchSectionAdapterのみ更新通知され、TextSectionAdapterの方では更新処理がされていないことが分かります。

(SwitchSectionAdapterでなぜ二回ItemCallbackが呼ばれているのかは分かりません。。)

D/buttonSectionAdapter: onClicked
D/SwitchSectionAdapter: ItemCallback
D/SwitchSectionAdapter: ItemCallback

ViewTypeは指定しなくて良いのか

1つのAdapterないでViewHolderを使い分ける時にはvewTypeを使い分けていましたが、MergeAdapterを使う場合には不要なのでしょうか。

結論から言うと、mergeされる各Adapter間で異なるViewHolderを使うのであれば不要です。

MergeAdapterの初期化にConfigを設定でき、さらにその中でisolateViewTypesというフラグを指定することができます。

By default, it is set to true which means MergeAdapter will isolate view types across adapters, preventing them from using the same ViewHolders.

このフラグにより各AdapterでviewTypeを独立させるかどうかを指定でき、デフォルトのConfig(Config.DEFAULT)では isolateViewTypes = true になっています。

val mergeAdapter = MergeAdapter(
            // このように明示的にViewTypeを独立させることを指定することもできるが、指定無しでもデフォルトでそうなっている
            MergeAdapter.Config.Builder().setIsolateViewTypes(true).build(),
            textSectionAdapter,
            buttonSectionAdapter,
            switchSectionAdapter
        )

なお、このisolateViewTypesがtrueの場合、以下のようにしてMergeAdapter側でadapterごとにViewTypeの値を++して区別するようにしています。


|  class IsolatedViewTypeStorage implements ViewTypeStorage { |
|:--|
|         SparseArray<NestedAdapterWrapper> mGlobalTypeToWrapper = new SparseArray<>(); |
|         int mNextViewType = 0; |
|         int obtainViewType(NestedAdapterWrapper wrapper) { |
              // AdapterごとにViewTypeを++している
|             int nextId = mNextViewType++; |
|             mGlobalTypeToWrapper.put(nextId, wrapper); |
|             return nextId; |
|         } |

逆に、adapter間でViewHolderを共有したい場合には、setIsolateViewTypes(false)にし、各adapterで明示的にidを指定してあげる必要があります。

参考:https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a

まとめ

このように、MergeAdapterを使うことで、複数のViewHolderを使ってのRecyclerViewが扱いやすくなりました。

皆さんもぜひ使ってみてください。

なお、今回作ったサンプルコードはこちら

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3