［Android］Epoxy のサンプルと解説 (CustomView から作成)

# はじめに

RecyclerView を使う際には Adapter などに独自で処理を実装する必要があり、ここが RecyelerView を利用する上でかなり面倒な実装だったりします。 Epoxy はこの面倒な処理を実装してくれるライブラリらしいです。Epoxy のサンプルを作成してどんなことができるのかざっくりと解説していきたいと思います。

# 準備する

Epoxy を利用できるようにセットアップを進めます。ライブラリとして epoxy、 epoxy-databinding, epoxy-processor を追加します。databinding を利用する際には epoxy-databinding が必要になります、databinding を利用しないのであれば追加する必要はないです。ライブラリを追加したら kapt と databinding を有効化するだけです。これでセットアップは完了です。

```groovy
// kapt 有効化
apply plugin: 'kotlin-kapt' 

android {
      ︙
    // databinding 有効化
    dataBinding {
        enabled = true 
    }
      ︙
}

dependencies {
      ︙
    def epoxy_version = "3.9.0"
    implementation "com.airbnb.android:epoxy:$epoxy_version"
    implementation "com.airbnb.android:epoxy-databinding:${epoxy_version}"
    kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
      ︙
}
```

# 実装する

Epoxy を利用するには EpoxyModel とEpoxyController を作成する必要があります。
これら EpoxyModel と EpoxyController ですが次のような役割を担当します。

| 名称| 説明 |
| ------- | ------- | 
| EpoxyModel  | RecylerView に表示する View を定義する。また Epoxy から View を操作するためのインタフェースを定義する。 | 
| EpoxyController | RecyclerView に表示する EpoxyModel を生成し、EpoxyModelに定義されたインタフェースを使って、View の操作を行う。 |

## EpoxyModel を作成する

EpoxyModel を作成する方法は 3種類あるみたいです。CustomViewから作成する方法、Databinding から作成する方法、 ViewHolder から作成する方法とあります。今回は CustomView から作成するのを試してみたいと思います。

[![Image from Gyazo](https://i.gyazo.com/70e0179f24c4c212d917126f72f09d9a.png)](https://gyazo.com/70e0179f24c4c212d917126f72f09d9a)

CustomView から生成する場合には、まずは普通に CustomView を作成します。今回は TextView と Button を持った CustomView を用意しています。

```kotlin
class HeaderCustomView : LinearLayout {
    private lateinit var titleView: TextView
    private lateinit var buttonView: Button

    constructor(context: Context) : super(context) {
        init(context, null)
    }

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        init(context, attrs)
    }

    private fun init(context: Context, attrs: AttributeSet?) {
        titleView = TextView(context, attrs).apply {
            this.width = 500
            this.textSize = 32f
        }

        buttonView = Button(context, attrs).apply {
            buttonView.text = "ACTION"
        }

        this.addView(titleView)
        this.addView(buttonView)
    }
}
```

CustomView が出来たら EpoxyModel を作成に必要な記述を追加していきます。CustomView に@ModelView をつけます、するとこの CustomView から EpoxyModel が生成されるようになります。

```kotlin
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class HeaderCustomView : LinearLayout {

}
```

Build ➔ Make Project からビルドしてみましょう。すると java ➔ generated ➔ project package ➔ view に HeaderCustomViewModel_ が生成されます。この生成された HeaderCustomViewModel_ が EpoxyModel になります。EpoxyController では この生成された HeaderCustomViewModel_ を利用して View の生成や操作を実装していきます。

```kotlin
public class HeaderCustomViewModel_ extends EpoxyModel<HeaderCustomView> implements GeneratedModel<HeaderCustomView>, HeaderCustomViewModelBuilder {
        ︙
}
```

EpoxyModel の生成は完了しましたが、この EpoxyModel には View を操作するためのメソッドが定義されていないので、 TextView の Text や Button の onClick を変更できない状態になっています。このままでは固定値で生成された View を表示するだけになってしまうので、 View の内容を変更できるようにメソッドを追加していきます。

Text を変更するならば @TextProp をつけたメソッド、Event を受け取る Listener を変更するならば @CallbackProp をつけたメソッドを用意してあげます。そうすると EpoxyModel に View を操作するメソッドが自動生成され、あとから Text や onClick を変更できるようになります。（アノテーションの具体的な説明は[こっち](https://github.com/airbnb/epoxy/wiki/Generating-Models-from-View-Annotations)に説明がありますので確認してみてください。）

```kotlin
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class HeaderCustomView : LinearLayout {
        ︙
    // Textを変更するならば @TextProp をつける
    @TextProp
    fun setTitle(text: CharSequence?) {
        titleView.text = text
    }

    // View.OnClickListener を変更するならば @CallbackProp をつける
    @CallbackProp
    fun onClickListener(listener: View.OnClickListener?) {
        buttonView.setOnClickListener(listener)
    }
        ︙
}
```

## EpoxyController を作成する

EpoxyModel の実装が完了したので、次は EpoxyController を作成していきます。EpoxyController の作成は簡単で Typed2EpoxyController を継承したクラスを作成してやります。あとは buildModels を override し EpoxyModel を使って View の生成をしてやるだけです。

```kotlin
class HeaderCustomViewController(
    private val selectListener: SelectListener
) : Typed2EpoxyController<List<String>, Boolean>() {

    override fun buildModels(names: List<String>, loadingMore: Boolean) {
        names.forEach { item ->
            headerCustomView{
                id("Content")
                title(item)
                onClickListener(View.OnClickListener { selectListener.onSelected(item) })
            }
        }
    }

    interface SelectListener {
        fun onSelected(item: String)
    }
}
```

# 動作確認する


まずは EpoxyController を生成していきます。EpoxyController を生成すると RecyclerView に設定する Adapter が用意されます。なので RecyclerView にその adapter をセットしてやります。あとは EpoxyControler の setData にてデータをセットしてやるだけです。これで setData にてセットしたデータが RecyclerView に表示されます。

```kotlin
class MainActivity : AppCompatActivity() {
    private val itemList = listOf(
        "ONE", "TWO", "THREE", "FOUR", "FIVE",
        "SIX", "SEVEN", "EIGHT", "NINE", "TEN"
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val headerCustomViewController = HeaderCustomViewController(object :
            HeaderCustomViewController.SelectListener {
            override fun onSelected(item: String) {
                Toast.makeText(applicationContext, item, Toast.LENGTH_SHORT).show()
            }
        })

        recycler_view.apply {
            this.adapter = headerCustomViewController.adapter
            this.layoutManager = LinearLayoutManager(applicationContext).apply {
                orientation = LinearLayoutManager.VERTICAL
            }
        }

        headerCustomViewController.setData(itemList, true)
    }
}
```

```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

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

起動してみます、はいセットしたデータが RecyclerView に問題なく表示されています。

[![Image from Gyazo](https://i.gyazo.com/69cffcae65524e338d1a4565b9517fed.png)](https://gyazo.com/69cffcae65524e338d1a4565b9517fed)

これで起動してみると、セットしたデータが RecyclerView に表示されるようになります。


# おわりに

CustomView から EpoxyModel を作成するパターンでは次の実装が必要

- CustomView から EpoxyModel を作成するには @ModelView アノテーションをつける
- CustomView から EpoxyModel を作成するときは @TextProp @CallbackProp をつけたメソッドを用意して EpoxyModel を生成した際したあとに View の Text や Callback リスナーなどを登録できるようにする必要がある。
- EpoxyController には与えられたデータから EpoxyModel をどのように生成するか定義する。

<a href="https://github.com/kaleidot725-android/epoxy"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/kaleidot725-android/epoxy.png" width="460px"></a>

# 参考文献

- [Epoxy | GitHub](https://github.com/airbnb/epoxy)