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

SNSのいいねボタンとか想像してもらえるとわかりやすいと思う。
押すたびにステータスが変わって、それに応じて見た目も変化して欲しい。
実装方法
特別な処理は入れていないけど、一応のっけとく。
◾︎SampleActivity
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
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 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 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
しているのに見た目が変わらない。
sampleEpoxyModel?.isSelected = sampleEpoxyModel?.isSelected?.not() ?: false
epoxyController?.setData(sampleEpoxyModels)
更新後の値を確認してもやっぱりきちんと変わっている。
一応idを変えればViewHolderごと再生性できるんだけど、それをするとちらついて鬱陶しい。
原因&解決方法
色々試した結果、リストのフラグは変わっていても、インスタンス自体は同じなのが原因だということがわかった。
どうやらEpoxyは、保持しているリストの中の値が変わったかどうかは見ていないようで、参照しているインスタンスが変更されたかどうかで変更を検知してるみたい。
==
とequals()
の違いだね。
ということは、変更する要素のindexに対して.copy()
で新たに生成したインスタンスを入れてあげれば変更が検知されるはず。
onClick = {
val index = sampleEpoxyModels?.indexOf(it) ?: -1
sampleEpoxyModels?.set(
index,
it.copy(
isSelected = it.isSelected.not()
)
)
epoxyController?.setData(sampleEpoxyModels)
}
こんな感じで修正したら、きちんと動きました!!
まとめ
Epoxyでフラグ使ってViewの見た目を更新するときはインスタンスごと再生成してあげましょう!
おわり