【Android】Epoxyのモデル変更判定

# はじめに
RecyclerViewを超絶簡単に実装できるEpoxyだけど、こいつを使い初めてから幾度となく**値を変更しても見た目が変更されない**不具合に悩まされ続けてきた。
その度になんとなく他の部分と同じように修正してどうにかしてきたけど、先日やっと根本的な原因がわかった。
もはや自分では忘れないと思うけど、何故か調べても同じようなことで悩んでいる人の記事とか出てこなかったので紹介する。

## やりたいこと
選択したやつの色を変えてあげたい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474683/54d1f85f-8c85-9542-6505-f0bdb38c207a.gif" width="300"/>

SNSのいいねボタンとか想像してもらえるとわかりやすいと思う。
押すたびにステータスが変わって、それに応じて見た目も変化して欲しい。

## 実装方法

特別な処理は入れていないけど、一応のっけとく。

◾︎SampleActivity

``` kotlin
class SampleActivity : AppCompatActivity() {

    private lateinit var binding: ActivitySampleBinding
    private var epoxyController: SampleEpoxyController? = null
    private var sampleEpoxyModels: List<SampleEpoxyModel>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_sample)
        setController()
    }

    private fun setController() {
        sampleEpoxyModels = (0..9).map {
            SampleEpoxyModel(it)
        }
        epoxyController = SampleEpoxyController(
            onClick = {
                val sampleEpoxyModel = sampleEpoxyModels?.find { sampleEpoxyModel ->
                    sampleEpoxyModel.id == it.id
                }
                sampleEpoxyModel?.isSelected = sampleEpoxyModel?.isSelected?.not() ?: false
                epoxyController?.setData(sampleEpoxyModels)
            }
        )
        binding.recyclerView.setController(epoxyController as EpoxyController)
        epoxyController?.setData(sampleEpoxyModels)
    }
}
```

◾︎SampleController

``` kotlin
class SampleEpoxyController(
    private val onClick: (SampleEpoxyModel) -> Unit
) : TypedEpoxyController<List<SampleEpoxyModel>?>() {

    override fun buildModels(data: List<SampleEpoxyModel>?) {
        data?.forEach { sampleEpoxyModel ->
            itemSampleList {
                id("EpoxyModel_id:${sampleEpoxyModel.id}")
                sampleEpoxyModel(sampleEpoxyModel)
                onClick { _ ->
                    onClick.invoke(sampleEpoxyModel)
                }
            }
            itemDivider {
                id("divider")
            }
        }
    }
}
```

◾︎ activity_sample.xml

``` xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <com.airbnb.epoxy.EpoxyRecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:splitMotionEvents="false"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintBottom_toBottomOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

◾︎ item_sample_list.xml

``` xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
                name="sampleEpoxyModel"
                type="com.kl.testapp2.epoxymodelchange.SampleEpoxyModel" />

        <variable
                name="onClick"
                type="android.view.View.OnClickListener" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:background="@{sampleEpoxyModel.isSelected ? @color/colorPrimary : @color/white}"
            android:onClick="@{onClick}" />
    
</layout>

```

リストに設定した`onClick`のイベントを受け取って、Activityが保持しているListに対してfindして更新するアイテムを見つける。
取得したアイテムのフラグを反転させて再度`setData()`を実行している。

## 不具合内容

中の値を入れ替えて、しっかり`setData`しているのに見た目が変わらない。

``` kotlin
sampleEpoxyModel?.isSelected = sampleEpoxyModel?.isSelected?.not() ?: false
epoxyController?.setData(sampleEpoxyModels)
```

更新後の値を確認してもやっぱりきちんと変わっている。
一応idを変えればViewHolderごと再生性できるんだけど、それをするとちらついて鬱陶しい。

## 原因&解決方法

色々試した結果、リストのフラグは変わっていても、インスタンス自体は同じなのが原因だということがわかった。
どうやらEpoxyは、保持しているリストの中の値が変わったかどうかは見ていないようで、参照しているインスタンスが変更されたかどうかで変更を検知してるみたい。
`==`と`equals()`の違いだね。

ということは、変更する要素のindexに対して`.copy()`で新たに生成したインスタンスを入れてあげれば変更が検知されるはず。

``` kotlin
onClick = {
    val index = sampleEpoxyModels?.indexOf(it) ?: -1
    sampleEpoxyModels?.set(
        index,
        it.copy(
            isSelected = it.isSelected.not()
        )
    )
    epoxyController?.setData(sampleEpoxyModels)
}
```

こんな感じで修正したら、きちんと動きました！！

# まとめ

Epoxyでフラグ使ってViewの見た目を更新するときはインスタンスごと再生成してあげましょう！

おわり