GroupieのDiffUtilを正しく利用するコードを記述する

[DroidKaigi 2019](https://github.com/DroidKaigi/conference-app-2019)、[DroidKaigi 2018](https://github.com/DroidKaigi/conference-app-2018)でも採用され、最近はプロダクトでもよく導入されるようになってきた [Groupie](https://github.com/lisawray/groupie) ですが、今回はそのGroupieを使ううえで気をつける必要がある差分更新の実装について記述しました。

Groupieでは、RecyclewViewのDiffUtilを利用しItemの更新を検知し、その結果を使って追加や更新のアニメーションを行ってくれます。このとき、Itemの実装の仕方によりDiffUtilが意図したように動かず、不要なアニメーションが発生することがあります。

もし、そのような問題に直面している場合は、[RecyclerViewによるレイアウトを楽にするGroupieを1年使ってみて](https://qiita.com/takahirom/items/4125d7c871fad534d3c2#%E3%82%A2%E3%82%A4%E3%83%86%E3%83%A0%E3%81%A7%E5%A4%89%E6%9B%B4%E3%82%92%E6%A4%9C%E7%9F%A5%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AE%E3%83%AA%E3%83%BC%E3%82%AF%E9%98%B2%E6%AD%A2) という記事を参照して対策してみましょう。それらの問題についての原因と対策について記述されています。

今回の記事では、上記の記事とは別の方法での解決策を考えてみます。

## 問題はオブジェクトの再生成にある

上記の記事でも記述されている通り、以下のようなコードを記述した場合に `clickListener` が毎回別のオブジェクトとして再生成されるため、更新アニメーションがはいります。

```kotlin
        val handler = Handler()
        handler.postDelayed(object : Runnable {
            override fun run() {
                val clickListener: (View) -> Unit = { // 毎回**新しい**クリックリスナを作る
                    Log.d("Groupie!", "click:$it")
                }
                val items = listOf(
                        HeaderItem("Header1"),
                        TextItem("test1", clickListener), // クリックリスナを渡す
                        TextItem("test2", clickListener),
                        TextItem("test3", clickListener),
                        HeaderItem("Header2"),
                        TextItem("test4", clickListener),
                        TextItem("test5", clickListener)
                )
                groupAdapter.update(items)
                handler.postDelayed(this, 1000)
            }
        }, 1000)
    }
}
...
data class TextItem(
        val text: String,
        // 毎回作られるクリックリスナがdata classのequals()で比較されてfalseが返る！!!
        val clickListener: (View) -> Unit 
) : BindableItem<ItemTextBinding>(
        text.hashCode().toLong()
) {
    override fun getLayout() = R.layout.item_text

    override fun bind(viewBinding: ItemTextBinding, position: Int) {
        viewBinding.textView.text = text
        viewBinding.textView.setOnClickListener(clickListener)
    }
}
```

ここで問題なのが、**毎回リスナを生成しなおしている**ことにあります。

さきほど紹介した記事では、 オブジェクトの再生成を許容し `equals` と `hashCode` を工夫することで対策していました。そのため自由度が高く、インターフェースの実装さえ忘れなければ、コードの変更コストがアイテム内だけで完結し、アイテムを生成する側のリファクタリングコストが発生しません。

では、オブジェクトの再生成を許容しない実装も考えてみます。

## オブジェクトが再生成されないようにする

さきほどのコードを見てみると `clickListener` は引数で渡されるものしか使わないため (今回は説明のためにあえて毎回あたらしいリスナを作っていたが) 毎回生成する必要がない。 そのため、handler (アイテムが再生成されるスコープ) の外で `clickListener` を生成するようにしてみます。

```kotlin
        val clickListener: (View) -> Unit = { // 毎回新しいクリックリスナを作らない
            Log.d("Groupie!", "click:$it")
        }
        val handler = Handler()
        handler.postDelayed(object : Runnable {
            override fun run() {
                val items = listOf(
                        HeaderItem("Header1"),
                        TextItem("test1", clickListener), // クリックリスナを渡す
                        TextItem("test2", clickListener),
                        TextItem("test3", clickListener),
                        HeaderItem("Header2"),
                        TextItem("test4", clickListener),
                        TextItem("test5", clickListener)
                )
                groupAdapter.update(items)
                handler.postDelayed(this, 1000)
            }
        }, 1000)
    }
}

```

このようにすると、新しいアイテムに渡されるリスナが同じになるため問題が発生しなくなります。また、同じ挙動をするインスタンスを複数生成しなくなるためメモリの使用率も下がります。紹介した記事の対策では、アイテムを生成する側のリファクタリングが必要ありませんが、こちらの場合は、アイテム側のリファクタリングが必要なくなります。

## リスナをどうしても再生成したい

リスナをどうしても毎回新しいものにしたい場合は、紹介した記事の対策方法があります。また、今回でいうとデータクラスのコンストラクタにリスナを渡してしまっていたため、リスナがデータとして扱われているというところにも問題があります。そのため、できることなら以下のようにアイテム自体にメソッドを生やすのが良さそうです。

```kotlin
data class TextItem(
        val text: String
) : BindableItem<ItemTextBinding>(
        text.hashCode().toLong()
) {
    override fun getLayout() = R.layout.item_text

    override fun bind(viewBinding: ItemTextBinding, position: Int) {
        viewBinding.textView.text = text
        viewBinding.textView.setOnClickListener(onClick)
    }

    // アイテム自体にメソッドを生やす
    fun onClick(view: View) {
        Log.d("Groupie!", "click:$view")
    }
}
```

また、リスナをコンストラクタ以外で保持する方法をとれば、外からの注入もできます。以下のようにListenerインターフェースを定義し `notifyChanged` でListenerを実装したオブジェクトを渡します。

```kotlin
data class TextItem(
        val text: String 
) : BindableItem<ItemTextBinding>(
        text.hashCode().toLong()
) {
    // 再度bindされたときにも利用できるようListenerを保持しておく必要がある
    private val listenerHolder = AtomicReference<Listener>()

    override fun getLayout() = R.layout.item_text

    override fun bind(viewBinding: ItemTextBinding, position: Int) {
        viewBinding.textView.text = text
        viewBinding.textView.setOnClickListener(listenerHolder.get())
    }

    // notifyDataChangedでpalyLoadsにListenerが渡される
    override fun bind(viewBinding: ItemTextBinding, position: Int, payloads: MutableList<Any>) {
        // palyLoadsからListenerを見つけて保持する
        payloads.forEach { if (it is Listener) listenerHolder.set(it) }
        super.bind(viewBinding, position, payloads)
    }

    // Listenerを用意する
    interface Listener {
        val clickListener: (View) -> Unit
    }
}
```

ただし、この場合はListenerを渡さないとクリックリスナが設定されないので注意しましょう。

ちなみにこの方法でも、実装したListenerが再生成されないようにオブジェクト化することで、データクラスのコンストラクタに渡せます。すると `listenerHolder` は必要なくなるので安全です。

## もう `data class` をやめる

紹介した記事では `equals` と `hashCode` を上書きすることで挙動を変えていました。つまり、それらを自身で実装しているのとあまり変わらないため、そもそも `data class` である必要もないのではないだろうか。つまり `data class` をやめて  `equals` と `hashCode` を上書きするのだ(同じ)。ちなみにその場合は `data class` の他の恩恵を受けられなくなります (`toString`、`copy`など)。

## 結局どうすれば良いと思っているか

個人的には、リスナを外から注入する場合に再生成をしてしまってる場合は、そもそもコードの書き方や設計が間違っている可能性があると考えています。そのため、まずは **そのリスナが本当に再生成する必要があるのか** を確かめてみましょう。また、これからも正しくコードを書くために、GroupieのItemのこと、RecycerViewのDiffUtilのこと、RecyclerViewのこと、Kotlinとデータクラスのこと、などを詳しく調べておくと良いと思います。