---
# System prepended metadata

title: epoxy/kotlinsample

---

# epoxy/kotlinsample

ここでは、Epoxyのkotlinsampleを題材にepoxyを使ったRecyclerViewの構築方法を解説します。

https://github.com/airbnb/epoxy/tree/master/kotlinsample


<img src="https://i.imgur.com/REaizAp.png" width=30%>

## MainActivity

`MainActivity`では、複数のビュータイプをもつRecyclerViewをepoxyを使って構築します。

```xml=
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#80000000"/>
    <com.airbnb.epoxy.EpoxyRecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
```

このサンプルでは`EpoxyRecyclerView`を使用しています。`EpoxyRecyclerView`を使用するとepoxyとRecyclerViewを簡単に統合することができます。
`EpoxyRecyclerView`には以下のような特徴があります。

- 全てのEpoxyRecyclerViewインスタンスはViewPoolで共有されます。
- レイアウトマネージャーは、正常なデフォルトで自動的に追加されます。
- このビューのサイズがMATCH_PARENTの場合、setHasFixedSizeがtrueになります
- GridLayoutManagerが使用されている場合、これによりスパンカウントがEpoxyControllerと自動的に同期されます(RecyclerViewへのspanCountの設定が不要になる)。
- EpoxyControllerクラスを作成せずにモデルを設定するためのヘルパーメソッド。
- EpoxyControllerを設定し、1つのステップでモデルを構築します(初回のnotifyが不要になる)
- setItemSpacingPxメソッドによって全てのアイテムの間隔を簡単に設定できます
- ネストされたrecyclerviewとして使用するためのデフォルトは、カルーセルで提供されます。
- setClipToPaddingがデフォルトでfalseに設定されています

### Modelの作成

参考： https://github.com/airbnb/epoxy/wiki/Generating-Models-from-View-Annotations

RecyclerViewには以下の8種類のモデル(ビュー)を表示します

1. ColoredSquareView
2. databindingItem
3. ItemCustomView
4. ItemEpoxyHolder
5. ItemViewBindingEpoxyHolder
6. CarouselItemCustomView
7. ItemDataClass
8. ItemViewBindingDataClass

#### ColoredSquareView

<img src="https://i.imgur.com/N09mTH9.png" width=30%>

ColoredSquareViewは、カスタムビューからモデルを作成しています。

```kotlin=
// ColoredSquareView.kt
@ModelView(defaultLayout = R.layout.colored_square_view)
class ColoredSquareView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
...
}
```

```xml=
<!-- colored_square_view.xml -->
<?xml version="1.0" encoding="utf-8"?>
<com.airbnb.epoxy.kotlinsample.models.ColoredSquareView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="56dp"
    android:layout_height="56dp" />
```

カスタムビューからのモデルの生成には`ModelView`アノテーションを使用します。
`ModelView`アノテーションには、`defaultLayout`としてレイアウトのリソースを渡すことができます。`defaultLayout`にレイアウトを渡すと、epoxyによってinflateされます。
`defaultLayout`に渡すレイアウトは、カスタムビュー自身のみを一つだけ持ったものである必要があります。

```kotlin=
// ColoredSquareView.kt
@JvmOverloads
@ModelProp
fun color(@ColorInt color: Int = Color.RED) {
    setBackgroundColor(color)
}
```

動的に値を設定するプロパティには`prop`アノテーションを追加します。`prop`でマークされたプロパティは、epoxyがモデルを自動生成した際にsetterが用意されます。

カスタムビューのカラーを設定する`color`メソッドには、`@ModelProp`アノテーションを付け、コントローラーから呼び出せるようにします。
また、デフォルト引数を設定する場合には`@JvmOverLoads`アノテーションも追加する必要があります。

##### モデルへの値の設定

`@TextProp`や`@ModelProp`などのアノテーションでマークしたプロパティは、Controllerから値を設定することができます。

ColoredSquareViewではcolorを`@ModelProp`でマークしました。

```kotlin=
coloredSquareView {
    ...
    color(Color.DKGRAY)
}

// or

ColoredSquareViewModel_()
    .id("uniqueId")
    .color(Color.LTGRAY)
    .addTo(this)
```

コントローラーからはこのように値を設定できます。

#### databindingItem

<img src="https://i.imgur.com/TlifLez.png" width=３0%>

databindingItemは、databindingのレイアウトからモデルを作成しています。

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

    <data>

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

    </data>

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

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="20dp"
            android:onClick="@{onClick}"
            android:text="@{text}"
            android:background="?attr/selectableItemBackground"/>
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#80000000"/>
    </LinearLayout>

</layout>
```

```kotlin=
// EpoxyDataBindingPatterns.kt
@EpoxyDataBindingPattern(rClass = R::class, layoutPrefix = "epoxy_layout")
object EpoxyDataBindingPatterns
```

databindingのレイアウトからモデルを作成する場合、プロジェクト内の任意の場所に`@EpoxyDataBindingPattern`をつけたobjectを作成します。
`@EpoxyDataBindingPattern`には`layoutPrefix`として、モデル生成の対象とするレイアウトを指定します。
ここでは、`epoxy_layout`をprefixに持つレイアウトをモデル生成の対象としています。

#### ItemCustomView

<img src="https://i.imgur.com/VvopMaX.png" width=３0%>

ItemCustomViewは、カスタムビューからモデルを作成しています。

```kotlin=
// ItemCustomView.kt
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class ItemCustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    ...

     init {
        inflate(context, R.layout.custom_view_item, this)
        orientation = VERTICAL
        textView = (findViewById(R.id.title))
        textView.setCompoundDrawables(null, null, onVisibilityEventDrawable, null)
        textView.compoundDrawablePadding = (4 * resources.displayMetrics.density).toInt()
    }
}
```

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

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/selectableItemBackground"
        android:padding="20dp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#80000000" />
</merge>
```

ここでは、`@ModelView`アノテーションの`autoLayout`でビューのサイズを指定しています。
このように、通常のカスタムビューと同じくカスタムビュー内部でレイアウトをinflateすることも可能です。



```kotlin=
var listener: View.OnClickListener? = null
        @CallbackProp set

@TextProp
lateinit var title: CharSequence
```

setterやlateinitのプロパティに`@TextProp`や`@CallbackProp`アノテーションを付けることで、コントローラーから値を設定することができます。

```kotlin=
@AfterPropsSet
fun useProps() {
    // This is optional, and is called after the annotated properties above are set.
   // This is useful for using several properties in one method to guarantee they are all set first.
    textView.text = title
    textView.setOnClickListener(listener)
}
```

`@AfterPropsSet`アノテーションをつけたメソッドは、ビューのインスタンスがモデルにバインドされ、全てのプロパティが設定された後に実行されます。


```kotlin=
@OnVisibilityStateChanged
fun onVisibilityStateChanged(
    @VisibilityState.Visibility visibilityState: Int
) {
    ...
}
```

`@OnVisibilityStateChanged`アノテーションを付けたメソッドは、ビューのスクリーン上での表示状態が変わった際に実行されます。
表示状態には以下のものがあります。
https://github.com/airbnb/epoxy/wiki/Visibility-Events

- **Visible**：ビューの少なくとも1つのピクセルが表示されているときにトリガーされます。
- **Invisible**：ビューの画面にピクセルがなくなったときにトリガーされます。
- **Focused**：コンポーネントがフォーカス範囲に入ったときに発生するイベント。これは、コンポーネントがビューポートの半分以上を占めるか、コンポーネントがビューポートの半分よりも小さい場合は、コンポーネントが完全に表示されたときに発生します。
- **Unfocused Visible**：コンポーネントがフォーカス範囲を終了したときにトリガーされるイベント。 フォーカス範囲は、ビューポートの少なくとも半分として定義されます。
- **Full Impression Visible**：コンポーネントが全インプレッション範囲に入るとトリガーされるイベント。 これは、たとえば垂直RecyclerViewの場合、コンポーネントの上端と下端の両方が表示されるときに発生します。

#### ItemEpoxyHolder

<img src="https://i.imgur.com/8BOCfg4.png" width=３0%>

ItemEpoxyHolderはViewHolderパターンのような実装でモデルを作成しています。

```kotlin=
// ItemEpoxyHolder.kt
@EpoxyModelClass(layout = R.layout.view_holder_item)
abstract class ItemEpoxyHolder : EpoxyModelWithHolder<Holder>() {

    @EpoxyAttribute lateinit var listener: () -> Unit
    @EpoxyAttribute lateinit var title: String

    override fun bind(holder: Holder) {
        holder.titleView.setText(title)
        holder.titleView.setOnClickListener { listener() }
    }
}

class Holder : KotlinEpoxyHolder() {
    val titleView by bind<TextView>(R.id.title)
}
```

ViewHolderパターンのようにモデルを作成するには`EpoxyModelWithHolder`を継承したクラスを作成し、`@EpoxyModelClass`アノテーションを付けます。
プロパティには`@EpoxyAttribute`を設定することで、コントローラーから値を設定することができます。

#### ItemViewBindingEpoxyHolder

<img src="https://i.imgur.com/OP5NPGH.png" width=３0%>

ItemViewBindingEpoxyHolderはViewBindingを使用したViewHolderパターンでモデルを作成しています。

```kotlin=
// ItemViewBindingEpoxyHolder.kt
@EpoxyModelClass(layout = R.layout.view_binding_holder_item)
abstract class ItemViewBindingEpoxyHolder : ViewBindingEpoxyModelWithHolder<ViewBindingHolderItemBinding>() {

    @EpoxyAttribute lateinit var listener: () -> Unit
    @EpoxyAttribute lateinit var title: String

    override fun ViewBindingHolderItemBinding.bind() {
        title.text = this@ItemViewBindingEpoxyHolder.title
        title.setOnClickListener { listener() }
    }
}
```

ViewBindingを使用したViewHolderパターンでモデルを作成するには`ViewBindingEpoxyModelWithHolder`を継承したクラスを作成し、`@EpoxyModelClass`アノテーションを付けます。
プロパティには`@EpoxyAttribute`を設定することで、コントローラーから値を設定することができます。

#### CarouselItemCustomView

<img src="https://i.imgur.com/u6USQKf.png" width=３0%>

CarouselItemCustomViewはItemCustomViewと同様にカスタムビューからモデルを作成しています。

#### ItemDataClass

<img src="https://i.imgur.com/TTc8C6J.png" width=３0%>

ItemDataClassはアノテーションを使用せずにモデルを作成しています。

```kotlin=
// ItemDataClass.kt
data class ItemDataClass(
    val title: String
) : KotlinModel(R.layout.data_class_item) {

    val titleView by bind<TextView>(R.id.title)

    override fun bind() {
        titleView.text = title
    }
}

```

```kotlin=
// KotlinModel.kt
abstract class KotlinModel(
    @LayoutRes private val layoutRes: Int
) : EpoxyModel<View>() {

    private var view: View? = null

    abstract fun bind()

    override fun bind(view: View) {
        this.view = view
        bind()
    }

    override fun unbind(view: View) {
        this.view = null
    }

    override fun getDefaultLayout() = layoutRes

    protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> {
        override fun getValue(thisRef: KotlinModel, property: KProperty<*>): V {
            // This is not efficient because it looks up the view by id every time (it loses
            // the pattern of a "holder" to cache that look up). But it is simple to use and could
            // be optimized with a map
            @Suppress("UNCHECKED_CAST")
            return view?.findViewById(id) as V?
                ?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
        }
    }
}
```

この方法では、`EpoxyModel`を継承した`KotlinModel`を作成しています。`KotlinModel`では`EpoxyModel`の`bind`, `unbind`, `getDefaultLayout`をオーバーライドし、ビューのバインド処理を行なっています。また、抽象メソッドとして`bind`を新たに定義し、ビューのバインド時に実行します。

`ItemDataClass`はepoxyの差分更新のためにdata classとして定義する必要があります。

#### ItemViewBindingDataClass

<img src="https://i.imgur.com/5nagiYJ.png" width=３0%>

ItemViewBindingDataClassでは、アノテーションを使用せずにViewBinding利用したモデル作成を行なっています。

```kotlin=
// ItemViewBindingDataClass.kt
data class ItemViewBindingDataClass(
    val title: String
) : ViewBindingKotlinModel<DataClassViewBindingItemBinding>(R.layout.data_class_view_binding_item) {
    override fun DataClassViewBindingItemBinding.bind() {
        title.text = this@ItemViewBindingDataClass.title
    }
}
```

```kotlin=
// ViewBindingKotlinModel.kt
abstract class ViewBindingKotlinModel<T : ViewBinding>(
    @LayoutRes private val layoutRes: Int
) : EpoxyModel<View>() {
    // Using reflection to get the static binding method.
    // Lazy so it's computed only once by instance, when the 1st ViewHolder is actually created.
    private val bindingMethod by lazy { getBindMethodFrom(this::class.java) }

    abstract fun T.bind()

    @Suppress("UNCHECKED_CAST")
    override fun bind(view: View) {
        var binding = view.getTag(R.id.epoxy_viewbinding) as? T
        if (binding == null) {
            binding = bindingMethod.invoke(null, view) as T
            view.setTag(R.id.epoxy_viewbinding, binding)
        }
        binding.bind()
    }

    override fun getDefaultLayout() = layoutRes
}

// Static cache of a method pointer for each type of item used.
private val sBindingMethodByClass = ConcurrentHashMap<Class<*>, Method>()

@Suppress("UNCHECKED_CAST")
@Synchronized
private fun getBindMethodFrom(javaClass: Class<*>): Method =
    sBindingMethodByClass.getOrPut(javaClass) {
        val actualTypeOfThis = getSuperclassParameterizedType(javaClass)
        val viewBindingClass = actualTypeOfThis.actualTypeArguments[0] as Class<ViewBinding>
        viewBindingClass.getDeclaredMethod("bind", View::class.java)
            ?: error("The binder class ${javaClass.canonicalName} should have a method bind(View)")
    }

private fun getSuperclassParameterizedType(klass: Class<*>): ParameterizedType {
    val genericSuperclass = klass.genericSuperclass
    return (genericSuperclass as? ParameterizedType)
        ?: getSuperclassParameterizedType(genericSuperclass as Class<*>)
}
```

`EpoxyModel`を継承した`ViewBindingKotlinModel`では、Bindingインスタンスの作成と保存、取り出しを行います。

### Controller

```kotlin=
recyclerView.withModels {
    ...
}
```

`MainActivity`では、`EpoxyRecyclerView#withModels`を使用してコントローラを作成し、モデルの設定を行なっています。

Epoxyでは、コントローラーにモデルを追加した順がRecyclerViewへのビューの表示順となります。
アノテーションによって自動生成されたモデルやpropsは、Kotlin DSLを使用して設定することが可能です。モデルをコントローラーに追加する際には一意なidを設定する必要があります。

```kotlin=
group {
    id("epoxyModelGroupDsl")
    layout(R.layout.vertical_linear_group)
    coloredSquareView {
        id("coloredSquareView 1")
        color(Color.DKGRAY)
    }
    coloredSquareView {
        id("coloredSquareView 2")
        color(Color.GRAY)
    }
    coloredSquareView {
        id("coloredSquareView 3")
        color(Color.LTGRAY)
    }
}
```
1,2段目には、`ColoredSquareView`を挿入しています。
`group`は、epoxyで用意されているモデルをグループ化するメソッドです。サンプルでは、3つの`ColoredSquareView`を横に並べグループ化しています。

```kotlin=
for (i in 0 until 100) {
    dataBindingItem {
        id("data binding $i")
        text("this is a data binding model2")
        onClick { _ ->
            Toast.makeText(this@MainActivity, "clicked", Toast.LENGTH_LONG).show()
        }
        onVisibilityStateChanged { model, view, visibilityState ->
            Log.d(TAG, "$model -> $visibilityState")
        }
    }
    itemCustomView {
        id("custom view $i")
        color(Color.GREEN)
        title("Open sticky header activity")
        listener { _ ->
            Toast.makeText(this@MainActivity, "clicked", Toast.LENGTH_LONG).show()
            startActivity(Intent(this@MainActivity, StickyHeaderActivity::class.java))
        }
        ...
}
```

3段目以降は

1. dataBindingItem
2. ItemCustomView
3. ItemEpoxyHolder
4. ItemViewBindingEpoxyHolder
5. CarouselItemCustomView
6. ItemDataClass
7. ItemViewBindingDataClass

の順に並べられたブロックを、forで指定した回数分追加しています。

## StickyHeaderActivity

`MainActivity`に表示されるItemCustomViewをタップすると`StickyHeaderActivity`に遷移することができます。

### Controller

`StickyHeaderActivity`ではコントローラークラスを作成しています。

```kotlin=
class StickyHeaderController(
    private val context: Context
) : EpoxyController(), StickyHeaderCallbacks {

    override fun buildModels() {
        for (i in 0 until 100) {
            when {
                i % 5 == 0 -> stickyItemEpoxyHolder {
                    id("sticky-header $i")
                    title("Sticky header $i")
                    listener {
                        Toast.makeText(context, "clicked", Toast.LENGTH_LONG).show()
                    }
                }
                else -> itemEpoxyHolder {
                    id("view holder $i")
                    title("this is a View Holder item")
                    listener {
                        Toast.makeText(context, "clicked", Toast.LENGTH_LONG)
                            .show()
                    }
                }
            }
        }
    }

    // Feel feel to use any logic here to determine if the [position] is sticky view or not
    override fun isStickyHeader(position: Int) = adapter.getModelAtPosition(position) is StickyItemEpoxyHolder
}
```

`EpoxyController`を継承した`StickyHeaderController`では`buildModels`を実装します。`buildModels`はモデルの設定を行い、 `reuestModelBuild`をトリガーに実行されます。
さらに、`StickyHeaderController`では、StickyHeaderの実現のため`StickyHeaderCallbacks#isStickyHeader`を実装しています。

### RecyclerViewの設定

```kotlin=
class StickyHeaderActivity : AppCompatActivity() {
    private lateinit var recyclerView: EpoxyRecyclerView

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

        recyclerView = findViewById(R.id.recycler_view)

        recyclerView.layoutManager = StickyHeaderLinearLayoutManager(this)
        recyclerView.setController(StickyHeaderController(this))
        recyclerView.requestModelBuild()
    }
}
```

`StickyHeaderActivity`では、`EpoxyRecyclerView`に対し`StickyHeaderLinearLayoutManager`をLayoutManagerとして設定し。コントローラーは`StickyHeaderController`を設定しています。
コントローラーの設定後、`requestModelBuild`を実行して`StickyHeaderController`の`buildModels`をトリガーし、RecyclerViewへのアイテムの表示を行っています。


## リストの更新

参考： https://github.com/airbnb/epoxy/wiki/Diffing

モデルはImmutableとなっており、ビューからのモデルの更新はできません。コールバックを用いてビューのイベントを取得し、新たなモデルを作成する必要があります。


```kotlin=
class SampleController : EpoxyController() {

    var data: SampleData? = null

    override fun buildModels() {
        data?.messages?.forEachIndexed { i, v ->
            sample {
                id(i)
                message(v)
            }
        }
    }
}
```

```kotlin=
// MainActivity.kt
sampleController.data = data
sampleController.requestModelBuild()
```

EpoxyControllerを使用している場合は、データの更新後、controllerの`requestModelBuild`を実行することで自動で差分更新されます。


```kotlin=
class TypedSampleController : TypedEpoxyController<SampleData>() {
    override fun buildModels(data: SampleData) {
        data.messages.forEachIndexed { i, v ->
            sample {
                id(i)
                message(v)
            }
        }
    }
}
```

```kotlin=
// MainActivity.kt
typedSampleController.setData(data)
```

TypedEpoxyControllerを使用している場合は、データの更新後、controllerの`setData`にデータを渡すことで自動で差分更新されます(`requestModelBuild`は実行しないでください)

## おまけ

`withModels`の実装

```kotlin=
fun withModels(buildModels: EpoxyController.() -> Unit) {
    val controller = (epoxyController as? WithModelsController)
        ?: WithModelsController().also { setController(it) }
    controller.callback = buildModels
    controller.requestModelBuild()
}

private class WithModelsController : EpoxyController() {
    var callback: EpoxyController.() -> Unit = {}
    override fun buildModels() {
        callback(this)
    }
}
```
