Epoxyを使ってRecyclerViewのデータの扱いを楽にする

自作アプリでepoxyを使用したので、簡単に使用方法をまとめておきます。

# Epoxyとは
Airbib製のライブラリです。RecyclerViewをそのまま使うのに比べ、複数パターンのレイアウトがある場合に簡単に切り替えられたり、表示データの更新でいい感じに差分更新をしてくれます(後述)。
https://github.com/airbnb/epoxy

今回は以下のような構造のデータをリスト表示する例を書いてみます

```kotlin
data class Foo (
    var title: String,
    var bar: List<Bar>
)

data class Bar (
    var body: String,
    var time: String  
)
```

# gradle

build.gradle(app)

```gradle

apply plugin: 'kotlin-kapt' // kapt使用

android {
 dataBinding {
        enabled = true // data binding使います
    }
}

kapt {
    correctErrorTypes = true
}

dependencies {
    implementation 'com.airbnb.android:epoxy:3.4.2'
    kapt 'com.airbnb.android:epoxy-processor:3.4.2'
    implementation 'com.airbnb.android:epoxy-databinding:3.4.2' // data binding使います
}
```

# cellレイアウトの準備
## list_cell_a.xml
```xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="title"
            type="String"
            />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{title}"
            android:textSize="24sp"
            android:textStyle="bold"
            tools:text="test"
            />

    </LinearLayout>

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

    <data>

        <variable
            name="body"
            type="String"
            />

        <variable
            name="time"
            type="String"
            />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="8dp"
        >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{body}"
            android:textSize="20sp"
            tools:text="test"
            />


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{time}"
            android:textSize="18sp"
            tools:text="test"
            />
    </LinearLayout>

</layout>
```

## EpoxyConfig.java
パッケージフォルダのルートにこのファイルを配置することで、ビルド時にレイアウトファイルのクラスを`{レイアウトファイルのキャメルケース}BindingModel_`の形式で自動生成してくれます。

```java
package <your package name>;

import com.airbnb.epoxy.EpoxyDataBindingLayouts;

@EpoxyDataBindingLayouts({
    R.layout.list_cell_a,
    R.layout.list_cell_b
})
interface EpoxyConfig {}
```

# EpoxyController
AdapterのかわりにTypedEpoxyControllerを継承したクラスを作成します。`buildmodels`をoverrideし、レイアウト作成処理を実装します。

```kotlin
class FooBarController : TypedEpoxyController<Foo>() {
    override fun buildModels(foo: Foo) {

        ListCellABindingModel_()
            .title(foo.title) // ←databindingのデータ
            .id(modelCountBuiltSoFar)
            .addTo(this)

        foo.bar.forEach {
            ListCellBBindingModel_()
                    .body(it.body)
                    .time(it.time)
                    .id(modelCountBuiltSoFar)
                    .addTo(this)
        }
    }
}
```

- buildModelsメソッドの引数の数に応じて、Typed2EpoxyController、Typed3EpoxyControllerを継承してください(クラス名もっとどうにかならなかったのだろうか)
- title、body、timeはレイアウトで設定したdatabindingのプロパティです。
- idは各セルに割り振られるIDです。このidが同じだとセルのデータ更新、idが異なるとセルの新規作成となります。例えば以下のように記載すると、セルは一つしか生成されません。

```kotlin

override fun buildModels(foo: Foo) {

    ListCellABindingModel_()
        .title(foo.title)
        .id("id")
        .addTo(this)

    foo.bar.forEach {
        ListCellBBindingModel_()
                .body(it.body)
                .time(it.time)
                .id("id")
                .addTo(this)
    }
}
```

- 特に差分更新を意識する必要がない場合は、idを自動で割り振ってくれる`modelCountBuiltSoFar`を使うと良いみたいです

# 呼び出す画面
呼び出し側のlayoutファイルでは普通に`RecyclerView`を宣言します。

```kotlin

class FooBarFragment : Fragment() {
    lateinit var binding: FragmentFooBarBinding //←このフラグメントのレイアウトファイル
    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_foo_bar, container, false)
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        val controller = FooBarController()
        binding.recyclerView.adapter = controller.adapter

        val data = Foo("タイトル", listOf(Bar("本文1","時間1"),Bar("本文2","時間2")))
        controller.setData(data)
    })
}
```

- controller側のIDを適切に設定していれば、データの登録、更新全てで`setData`を呼べばいい感じにviewに反映してくれます。

# 所感
- viewHolderから開放されてスッキリ
- データ更新時のnotifyから開放されてスッキリ

# 参考
- https://qiita.com/tomo1139/items/2e3eb25cb81aa741f491