Airbnb製ライブラリのEpoxyでRecyclerViewのデータ更新を便利に

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

Epoxyとは

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

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

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

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

cellレイアウトの準備

list_cell_a.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 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>

package-info.java

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

@EpoxyDataBindingLayouts({
        R.layout.list_cell_a,
        R.layout.list_cell_b
})
package パッケージ名;

import com.airbnb.epoxy.EpoxyDataBindingLayouts;

EpoxyController

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

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

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

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

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

    foo.bar.forEach {
        ListCellBindingModel_()
                .body(it.body)
                .time(it.time)
                .id("id")
                .addTo(this)
    }
}
  • 特に差分更新を意識する必要がない場合は、idを自動で割り振ってくれるmodelCountBuiltSoFarを使うと良いみたいです

呼び出す画面

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

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)
        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から開放されてスッキリ
  • データ更新時のnotyfyから開放されてスッキリ

参考

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.