kako.dev

開発、自作アプリのこと

EpoxyのTips

Epoxyを使い始めて半年くらいたちまして、色々Epoxyの使い方わかってきたのでTipsとしてまとめます。

ヘッダー・フッターを表示したい

TypedEpoxyControllerの場合

buildModels で表示するアイテムの前後に入れてあげればそれがヘッダーフッターとして表示されます。

例えば、ヘッダーとして表示したいアイテムがItemHeaderModel_、フッターとして表示したいアイテムがItemFooterModel_としたこう書きます。

override fun buildModels(data: Items) {
    ItemHeaderModel_()
        .id("header")
        .addTo(this)

   // 表示したいアイテムの処理

   ItemFooterModel_()
        .id("footer")
        .addTo(this)

PagedListEpoxyControllerの場合

addModelsoverrideしてsuper.addModels(newModel)の前後に書きます。 buildItemModelじゃないところが微妙なハマりポイントです。

例えば、ヘッダーとして表示したいアイテムがItemHeaderModel_、フッターとして表示したいアイテムがItemFooterModel_としたこう書きます。

override fun addModels(models: List<EpoxyModel<*>>) {
    ItemHeaderModel_()
        .id("header")
        .addTo(this)

   super.addModels(newModel)

   ItemFooterModel_()
        .id("footer")
        .addTo(this)

アイテムにLiveDataを適応したい (ex.ヘッダーに表示件数を表示したい)

ViewModelに持たせているLiveDataの状態によりアイテムを変更させたい場合、まず、DataBindingViewModelを渡します。 次にinterfaceを介してLifecyclerOwnerをセットします。

例えば、ヘッダーのアイテムに表示件数を表示したい場合、Controller Fragment ViewModel XML はそれぞれこのような感じになります。

Controller

class MyController(val callback:  Callback, val viewModel: MyViewModel) : TypedEpoxyController<List<Item>>() {
    interface Callback {
        fun headerBind(view: DataBindingEpoxyModel.DataBindingHolder)
    }
    override fun buildModels(data: List<Item>) {
            ItemHeaderModel_()
                 .viewModel(viewModel)
                 .onBind { _, view, _ ->
                     callback.headerBind(view)
                 }
                .id("header")
                .addTo(this)
    }

Fragment

class MyFragment : Fragment(), MyController.Callback {
    private lateinit var viewModel: MyViewModel
    private val controller by lazy { MyController(this, viewModel) }
    
    override fun headerBind(view: DataBindingEpoxyModel.DataBindingHolder) {
         view.dataBinding.setLifecycleOwner(this) // interfaceにしてlifecycleOwnerにFragementをセットします。
    }

ViewModel

class MyViewModel : ViewModel() {
    val count: MutableLiveData<Int> = MutableLiveData()

XML

<layout>
    <data>
        <variable
            name="viewModel"
            type="com.sample.package.MyViewModel" />
    </data>
    <ConstraintLayout
           ... >
           <TextView
                text="@{viewModel.count}"
                ....

アイテムにお気に入り機能をつけたい

アイテムにお気に入り機能があり、クリックでオンオフのViewを変えたいとして、Viewだけを操作してもダメです。 Epoxyにアイテムが変わったことを通知させないといけません。

この場合EpoxyModelをカスタマイズします。

例えば、フラグでお気に入りのオンオフを管理するアイテムをFavoriteItemとして、カスタマイズするEpoxyModelFavoriteEpoxyModelとして書くとしたらこうなります。

XML(item_favorite.xml)

<layout>
    <data>
        <variable
            name="item"
            type="com.sample.package.FavoriteItem" />
        <variable
            name="favoriteClickListener"
            type="android.view.View.OnClickListener" />
    </data>
    ...
        <ImageView
            ...
            android:tint="@{item.isFavorite ? @color/FavoriteOn : @color/FavoriteOff}"
            android:onClick="@{favoriteClickListener}"
   ...

FavoriteItem

data class FavoriteItem(
    val id: Int
    var isFavorite: Boolean = false
    ...

FavoriteEpoxyModel

@EpoxyModelClass(layout = R.layout.item_favorite)
abstract class FavoriteEpoxyModel : DataBindingEpoxyModel() {
    @EpoxyAttribute lateinit var item: FavoriteItem
    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var favoriteClickListener: View.OnClickListener? = null

    override fun setDataBindingVariables(binding: ViewDataBinding?) {
        binding?.setVariable(BR.item, item)
        binding?.setVariable(BR.favoriteClickListener, favoriteClickListener)
    }
}

FavoriteController

    FavoriteEpoxyModel_()
        .item(item) // FavoriteItem
        .favoriteClickListener { model, parentView, clickedView, position ->
            callback.onFavoriteClickListener(position)
        }
        .id(item.id)

Fragment

override fun onFavoriteClickListener(position: Int) {
    // listの中から該当するアイテム見つけてお気に入りをオン

    // リスト更新
    controller.setData(newList)
}

ページングアイテムの途中に別のViewを挿入する

PagedListEpoxyControllerで表示するアイテムの途中に別のViewを挿入したい場合、リストそのものを操作します。

どうやるのかなとEpoxyのissueを見ていたらこちらを発見し、 https://github.com/airbnb/epoxy/issues/563#issuecomment-428319365 「Epoxy側でどうこうするよりKotlinでList操作するのが良いよ」ってことらしいです。

例えば、5つ目に別のアイテムを表示したい場合こうなります。

PagedListEpoxyController

override fun addModels(models: List<EpoxyModel<*>>) {
    // 挿入したい別のアイテム
    val otherModel = OtherModel_()
                      .id("other")

    val newModel = models.toMutableList()
    when (newModel.size < 5) {
        true -> newModel.add(otherModel) // アイテム数が5未満の場合は最後に追加
        false -> newModel.add(5, otherModel) // アイテム数が5以上の場合は5番目に追加
    }

    super.addModels(newModel)
    ...

以上です。