EpoxyでRecyclerViewでよく使う機能をサクッと実装する
DroidKaigi2018のアプリでGroupieが採用されたのを見て、RecyclerViewのライブラリをいくつか試してみたら、Epoxyが最高だったので、ご紹介したいと思います。
今回Epoxyを使ってEmptyView、ヘッダー、フッターを使った簡単なサンプルを作成したので、その実装方法を説明しながら、Epoxyの基本的な使い方を紹介します。
サンプルコード
今回の内容のサンプルコードはこちらになります。(Kotlinです) https://github.com/IsseiAoki/epoxy-sample
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Epoxyのサンプル作った <a href="https://t.co/KP4XoRVsCz">pic.twitter.com/KP4XoRVsCz</a></p>— Issei Aoki (@issei_greenwood) <a href="https://twitter.com/issei_greenwood/status/965103804923330589?ref_src=twsrc%5Etfw">2018年2月18日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>機能としてはざっくりと以下の4つです。
- ヘッダーに複数枚の画像を表示して横スワイプで切り替えられる
- アイテムはグリッド
- アイテムが無い時はEmptyViewを表示する
- 一番下までスクロールすると、ProgressBar付きのフッターを表示する
実装の基本的な流れ
Epoxyを使った場合の実装の基本的な流れは以下になります。
- build.gradleにEpoxyを追加
- レイアウトXMLを作成
- Epoxy用のModelを作成
- 一度ビルドする(Epoxyが必要なクラスを自動生成するのに必要)
- Epoxy用のControllerを作成
- RecyclerViewののセットアップ(Activity/Fragment)
今までAdapterにコードを書いていた所を、Epoxy用のModelとControllerに書くように変更するだけです。コード量的に増えたわけではないです。むしろ減ります。
インストール
build.gradleに以下を追加します
apply plugin: 'kotlin-kapt'
kapt {
correctErrorTypes = true
}
dependencies {
compile 'com.airbnb.android:epoxy:2.9.0'
kapt 'com.airbnb.android:epoxy-processor:2.9.0'
compile 'com.airbnb.android:epoxy-databinding:2.9.0'
}
レイアウトXMLの作成
レイアウトXMLの実装方法はEpoxyを使わない時と変わりません。
Activity
RecyclerView置いてるだけです
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:background="@color/colorPrimary"
>
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="@dimen/spacing_small"
android:paddingEnd="@dimen/spacing_small"
/>
</android.support.v4.widget.SwipeRefreshLayout>
</android.support.design.widget.CoordinatorLayout>
</layout>
ヘッダーに表示するカード
横スワイプ出来るヘッダーに表示されるカードのレイアウトです。 CardViewに画像を置いてるだけです。
<?xml version="1.0" encoding="utf-8"?>
<layout
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.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:selectableItemBackground"
app:cardBackgroundColor="@color/cardview_light_background"
app:cardCornerRadius="@dimen/card_corner_radius"
app:cardElevation="@dimen/cardview_default_elevation"
app:cardUseCompatPadding="true"
>
<com.isseiaoki.epoxy.component.AspectRatioImageView
android:id="@+id/banner_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:ariv_height_ratio="533"
app:ariv_width_ratio="800"
tools:src="@drawable/banner_1"
/>
</android.support.v7.widget.CardView>
</layout>
グリッドで表示するカード
グリッドで表示するのカードのレイアウトです。 CardViewに画像とテキストを置いてるだけです。
<?xml version="1.0" encoding="utf-8"?>
<layout
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.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:selectableItemBackground"
app:cardBackgroundColor="@color/cardview_light_background"
app:cardCornerRadius="@dimen/card_corner_radius"
app:cardElevation="@dimen/cardview_default_elevation"
app:cardUseCompatPadding="true"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<ImageView
android:id="@+id/item_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
tools:src="@drawable/cat_1"
/>
<TextView
android:id="@+id/item_text_view"
android:layout_margin="@dimen/spacing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</android.support.v7.widget.CardView>
</layout>
EmptyView
EmptyViewとして表示するレイアウトです。TextViewがあるだけです。
<?xml version="1.0" encoding="utf-8"?>
<layout>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/empty_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="no item"
/>
</FrameLayout>
</layout>
フッター
RecyclerViewを一番下までスクロールした時に読み込み中であることを示すためのフッターのレイアウトです。ProgressBarしかないです。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/spacing"
>
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:theme="@style/AppTheme.PopupOverlay"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</android.support.constraint.ConstraintLayout>
</layout>
Modelの作成
ここからEpoxy独自の実装になります。
EpoxyではModelクラスに、今まで RecyclerView.AdapterのonBindViewHolder
に書いていたコードを書いて、そこから Model_
クラスを生成して Controller
から利用します。
Epoxyでは、 Model_
クラスの生成方法として
- ViewHolder用のModelを実装して、そのクラスから生成
- カスタムビュー用のModelを実装して、そのクラスから生成
- DataBindingを用いてxmlファイルから生成(別途
package-info.java
を書く必要あり)
の3種類があります。
ButterKnifeを使用しているのであれば、ViewHolderを使う方法、DataBindingを使用しているのであれば、xmlから生成する方法を用いるのが勧められています。自分で使う時にカスタムビューから生成する方法はあまり使わないかもしれません。
今のプロジェクトでは、DataBindingを使用しているのですが、諸事情でCustomBindingAdapterを使わずViewHolder的な使い方をしているので、以下のようなカスタムModelを書く事でViewHolderを使う方法でDataBindingを使っています。
package com.isseiaoki.epoxy.recyclerview
import android.databinding.DataBindingUtil
import android.databinding.ViewDataBinding
import android.support.annotation.CallSuper
import android.view.View
import com.airbnb.epoxy.EpoxyHolder
class DataBindingEpoxyHolder : EpoxyHolder() {
@CallSuper
override fun bindView(itemView: View) {
binding = DataBindingUtil.bind(itemView)
}
lateinit var binding: ViewDataBinding
}
package com.isseiaoki.epoxy.recyclerview.model
import android.content.Context
import android.databinding.ViewDataBinding
import com.airbnb.epoxy.EpoxyModelWithHolder
import com.isseiaoki.epoxy.recyclerview.DataBindingEpoxyHolder
abstract class DataBindingModel<in T : ViewDataBinding> : EpoxyModelWithHolder<DataBindingEpoxyHolder>() {
abstract fun bind(binding: T, context: Context)
abstract fun unbind(binding: T)
@Suppress("UNCHECKED_CAST")
override fun bind(holder: DataBindingEpoxyHolder) {
val binding = holder.binding as? T ?: return
val context = binding.root.context
bind(binding, context)
}
@Suppress("UNCHECKED_CAST")
override fun unbind(holder: DataBindingEpoxyHolder) {
val binding = holder.binding as? T ?: return
unbind(binding)
}
}
EpoxyのModelは基本的にレイアウトと1対1の対応になっているので、レイアウトファイルの個数分のModelを作成します。今回のサンプルの場合は item_banner.xml
(ヘッダーの画像用のレイアウト) 、 item_empty_text.xml
(EmptyView用のレイアウト) 、 item_image.xml
(アイテムのレイアウト) 、 item_loading_footer.xml
(追加読み込み用のProgressのフッター用のレイアウト) の4つのレイアウトを作成しているので、Modelも BannerModel.kt
、EmptyTextModel.kt
、 ItemModel.kt
、LoadingFooterModel.kt
の4種類を作成します。
ItemModel
package com.isseiaoki.epoxy.recyclerview.model
import android.content.Context
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.isseiaoki.epoxy.R
import com.isseiaoki.epoxy.databinding.ItemImageBinding
import com.isseiaoki.epoxy.entity.SimpleItem
import com.squareup.picasso.Picasso
/**
* model for item
*/
@EpoxyModelClass(layout = R.layout.item_image)
abstract class ItemModel : DataBindingModel<ItemImageBinding>() {
@EpoxyAttribute lateinit var item: SimpleItem
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var onItemClicked: (SimpleItem) -> Unit
override fun bind(binding: ItemImageBinding, context: Context) {
Picasso.with(context).load(item.imageUrl).into(binding.itemImage)
binding.itemTextView.text = "cat ${item.id + 1}"
binding.root.setOnClickListener { onItemClicked(item) }
}
override fun unbind(binding: ItemImageBinding) {
binding.itemImage.setImageDrawable(null)
binding.root.setOnClickListener(null)
}
}
EpoxyModelWithHolder
を継承した abstract
クラスを実装しましょう。
まず @EpoxyModelClass(layout = R.layout.item_image)
でレイアウトXMLを指定して、bind()
の中に RecyclerView.Adapter
の onBindViewHolder
に書いていたコードを書きます。
ここでポイントとなるのは、 @EpoxyAttribute
で Model
の中で使用したい変数を定義する事です。 @EpoxyAttribute
を定義する事で、自動生成された Model_
クラス(この場合 ItemModel_
クラス)にsetterが生えて、後述する Controller
上で変数を渡せるようになります。
onItemClicked
にくっついている EpoxyAttribute.Option.DoNotHash
ですが、Epoxyは EpoxyAttribute
で渡される変数の状態を自動で比較して差分があった場合のみViewの更新が走るようになっており、 OnItemClicked
の変更がこのロジックに影響しないようにするため、EpoxyAttribute.Option.DoNotHash
をつけています。基本的にコールバックには全てつければ良さそうです。詳しい説明はwikiをご覧ください。
unbind()
では、 bind()
の中で重い処理をしている場合にここでキャンセルしたり、 Bitmap
や OnClickListener
を解放する事でパフォーマンスが上がります。bind()
の中でRxJavaの Disposable
を subscribe()
している場合はここでdispose()
してあげるのが良いと思います。
BannerModel
ItemModelとほぼ同じです。
package com.isseiaoki.epoxy.recyclerview.model
import android.content.Context
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.isseiaoki.epoxy.R
import com.isseiaoki.epoxy.databinding.ItemBannerBinding
import com.isseiaoki.epoxy.entity.SimpleItem
import com.squareup.picasso.Picasso
/**
* model for banner
*/
@EpoxyModelClass(layout = R.layout.item_banner)
abstract class BannerModel : DataBindingModel<ItemBannerBinding>() {
@EpoxyAttribute lateinit var banner: SimpleItem
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var onBannerClicked: (SimpleItem) -> Unit
override fun bind(binding: ItemBannerBinding, context: Context) {
Picasso.with(context).load(banner.imageUrl).into(binding.bannerImage)
binding.root.setOnClickListener { onBannerClicked(banner) }
}
override fun unbind(binding: ItemBannerBinding) {
binding.bannerImage.setImageDrawable(null)
binding.root.setOnClickListener(null)
}
}
LoadingFooterModel
ProgressBarを表示しているだけなので、何も渡すデータはなく、 bind()
、unbind()
でやることもありません。
package com.isseiaoki.epoxy.recyclerview.model
import android.content.Context
import com.airbnb.epoxy.EpoxyModelClass
import com.isseiaoki.epoxy.R
import com.isseiaoki.epoxy.databinding.ItemLoadingFooterBinding
/**
* model for footer
*/
@EpoxyModelClass(layout = R.layout.item_loading_footer)
abstract class LoadingFooterModel : DataBindingModel<ItemLoadingFooterBinding>() {
override fun bind(binding: ItemLoadingFooterBinding, context: Context) {
// do nothing
}
override fun unbind(binding: ItemLoadingFooterBinding) {
// do nothing
}
}
EmptyTextModel
EmptyViewなのでフッターと同じく渡してあげるデータはないのですが、複数のRecyclerViewで使い回せるようにテキストの内容を外から渡せるようにしてあります。
package com.isseiaoki.epoxy.recyclerview.model
import android.content.Context
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.isseiaoki.epoxy.R
import com.isseiaoki.epoxy.databinding.ItemEmptyTextBinding
/**
* model for empty view
*/
@EpoxyModelClass(layout = R.layout.item_empty_text)
abstract class EmptyTextModel : DataBindingModel<ItemEmptyTextBinding>() {
@EpoxyAttribute lateinit var emptyText: String
override fun bind(binding: ItemEmptyTextBinding, context: Context) {
if (emptyText.isNotEmpty()) {
binding.emptyTextView.text = emptyText
}
}
override fun unbind(binding: ItemEmptyTextBinding) {
// do nothing
}
}
ビルド
ここで一度ビルドしておきましょう。作成した Model
クラスから Model_
クラスが生成されるはずです。
Controllerの作成
いよいよ Controller
の作成です。それぞれのセルの表示に関してのコードはModelの中に既に書いてあるので、セルをどのような順番、どのような条件の時に表示するかを書いていきます。
package com.isseiaoki.epoxy.recyclerview.controller
import android.content.Context
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.CarouselModel_
import com.airbnb.epoxy.EpoxyController
import com.isseiaoki.epoxy.ext.dpToPx
import com.isseiaoki.epoxy.entity.SimpleItem
import com.isseiaoki.epoxy.recyclerview.model.BannerModel_
import com.isseiaoki.epoxy.recyclerview.model.EmptyTextModel_
import com.isseiaoki.epoxy.recyclerview.model.ItemModel_
import com.isseiaoki.epoxy.recyclerview.model.LoadingFooterModel_
import timber.log.Timber
class SimpleController(
var context: Context,
private var onBannerClicked: (SimpleItem) -> Unit = {},
private var onItemClicked: (SimpleItem) -> Unit = {},
filterDuplicates: Boolean = true
) : EpoxyController() {
init {
setFilterDuplicates(filterDuplicates)
}
private var banners = mutableListOf<SimpleItem>()
private var items = mutableListOf<SimpleItem>()
private var showFooter = false
override fun buildModels() {
// empty view
if (banners.isEmpty() || items.isEmpty()) {
EmptyTextModel_()
.id("empty")
.spanSizeOverride { _, _, _ -> 2 }
.emptyText("no item")
.addTo(this)
return
}
// carousel header
if (banners.isNotEmpty()) {
val spacing = context.dpToPx(8)
CarouselModel_()
.padding(Carousel.Padding(spacing, 0, 0, 0, spacing))
.id("carousel")
.spanSizeOverride { _, _, _ -> 2 }
.models(
banners.map {
BannerModel_()
.id(it.id)
.banner(it)
.onBannerClicked { onBannerClicked(it) }
}
)
.addTo(this)
}
// items
items.forEach {
ItemModel_()
.id(it.id)
.item(it)
.onItemClicked(onItemClicked)
.spanSizeOverride { _, _, _ -> 1 }
.addTo(this)
}
// footer
LoadingFooterModel_()
.id("footer")
.spanSizeOverride { _, _, _ -> 2 }
.addIf(showFooter, this)
}
fun update(banners: List<SimpleItem>, items: List<SimpleItem>) {
this.banners.clear()
this.banners.addAll(banners)
this.items.clear()
this.items.addAll(items)
showFooter = false
requestModelBuild()
}
fun addAll(items: List<SimpleItem>) {
this.items.addAll(items)
showFooter = false
requestModelBuild()
}
fun showFooter() {
showFooter = true
requestModelBuild()
}
fun showEmpty() {
requestModelBuild()
}
}
Controllerでポイントとなるのは buildModels()
です。
表示を行う際には必ず buildModels()
が呼ばれるようになっており、表示を変更したい場合には requestModelBuild()
を呼ぶことで buildModels()
が実行されます。
ControllerはActivity/Fragmentで保持して、データの更新がある度にControllerのメソッドを呼ぶことになるので、データの更新を行う際に必要なメソッドを用意しましょう。
今回のサンプルでは、
- 初期表示でEmptyViewを表示するための
showEmpty()
- 初回にデータ更新を行うための
update()
- リストが一番下までスクロールされた際にフッターを表示するための
showFooter()
- 追加読み込みが完了した際にデータを追加するための
addAll()
の4つを用意しました。
今度は buildModels()
に書いてあるコードについて説明していきましょう。
まず大雑把に説明すると、アイテムが何も無い時には、 EmptyTextModel_
を addTo(this)
することでEmptyViewを表示し、アイテムが存在する場合には、残りの BannerModel_
、 ItemModel_
、 LoadingFooterModel_
を addTo(this)
しています。
LoadingFooterModel_
については、リストが最後尾までスクロールされて、追加読み込みが完了するまでの間だけ表示したいので、addIf(showFooter, this)
を使って showFooter
が true
の場合だけControllerにaddしています。
次に Model_
のメソッドについて説明します。
id()
は、Modelごとの一意なIDを設定します。ここで渡しているIDは、RecyclerView.Adapterの getItemId(position: Int): Long
で返していた一意なIDと同じものです。
RecyclerView
ではsetHasStableId(true)
としてgetItemId(position: Int)
で一意なIDを返却することでパフォーマンスが向上するのですが、ここでidをセットすることで同じ効果が得られます。
ここで特筆すべきなのは、 Epoxyでは、ヘッダーやフッターなど、一意なIDが無いViewに関しては、文字列を渡すことによって自動的にIDを生成してくれることです。また、setFilterDuplicates(true)
を設定しておくことで、何らかの原因でリスト内に同じIDのアイテムが混入してしまった場合に、自動的に重複を省いてRecyclerViewのクラッシュを防いでくれます。
何か特別な理由が無い限りは基本的に id()
を設定しておくことでパフォーマンスが良くなるので設定することをおすすめします。
spanSizeOverride()
は、それぞれのアイテムの列数を設定するためのものです。
今回、 RecyclerView
には GridLayoutManager(context, 2)
を指定するのですが、
何もしないとヘッダー、フッターもアイテムと同じ2列で表示されてしまいます。
spanSizeOverride { _, _, _ -> 2 }
のように書いて、Activity/FragmentのRecyclerView
のセットアップ時に
GridLayoutManager().spanSizeLookup = controller.spanSizeLookup
と書くことで、このセルは2列分の幅を使って1列で表示して欲しいと指定することができます。
item()
や banner()
、 onItemClicked()
などはModel側で EpoxyAttribute
をつけた変数のsetterになります。
最後に Carousel
について説明します。
Carousel
はRecyclerViewのネストをより簡単に実現するためのEpoxy独自のカスタムビューで自動的に親のRecyclerViewとViewPoolをシェアしてくれます。
CarouselModel_
に生えている models()
にModelのリストを渡してやるだけで簡単に RecyclerView
のネストを作ることができます。ページャーぽく見えていたのは実はRecyclerViewでした。
ちなみに CarouselModel_
はライブラリの中に含まれているため、自分でModelを作成する必要はありません。
Carousel
に関する詳しい情報は、wikiをご覧ください。
RecyclerViewのセットアップ
あとはRecyclerViewに作成したControllerを設定してあげるだけです。
package com.isseiaoki.epoxy
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.widget.Toast
import com.isseiaoki.epoxy.databinding.ActivityMainBinding
import com.isseiaoki.epoxy.entity.SimpleItem
import com.isseiaoki.epoxy.ext.getUrlFromDrawableResId
import com.isseiaoki.epoxy.recyclerview.OnLoadMoreListener
import com.isseiaoki.epoxy.recyclerview.controller.SimpleController
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var controller: SimpleController? = null
private var sharedViewPool = RecyclerView.RecycledViewPool()
private val disposable = CompositeDisposable()
private var offset = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
setupRecyclerView()
loadData()
}
private fun setupRecyclerView() {
val nonnullController = SimpleController(
context = applicationContext,
onBannerClicked = {
Toast.makeText(this, "banner ${it.id} tapped", Toast.LENGTH_SHORT).show()
},
onItemClicked = {
Toast.makeText(this, "item ${it.id} tapped", Toast.LENGTH_SHORT).show()
}
)
controller = nonnullController
binding.recyclerView.apply {
clipChildren = false
setController(nonnullController)
setHasFixedSize(true)
recycledViewPool = sharedViewPool
val lm = GridLayoutManager(context, 2).apply {
spanSizeLookup = nonnullController.spanSizeLookup
recycleChildrenOnDetach = true
}
layoutManager = lm
clipToPadding = false
setItemSpacingDp(6)
addOnScrollListener(object : OnLoadMoreListener(lm) {
override fun onLoadMore() {
loadMore()
}
})
}
binding.refreshLayout.apply {
setOnRefreshListener {
offset = 0
loadData()
}
}
controller?.showEmpty()
}
いつもの setAdapter()
としている所を setController()
に変更するだけです。
データの更新を行いたい時は、Controllerに用意したメソッド達を呼んであげましょう。
まとめ
RecyclerViewをそのまま使おうとすると、自分で実装しなければいけないことが数多くあり、割とハードです。 Epoxyを導入することでボイラープレートコードを減らして幸せになりましょう!
ここは間違ってるよとか、これはこっちの方が良いよとかありましたら教えてください!
注意点
サンプルを作っている時に気づいたのですが、 Carousel
のアイテムに onMeasure
でViewのサイズを変更しているカスタムビューを使用すると(今回のサンプルではViewのアスペクト比が一定になるImageViewを使ってます)、 setNumViewsToShowOnScreen()
がうまく動かなくてハマったのでCarouselの使用は少し注意が必要だと感じました。
参考資料
https://github.com/airbnb/epoxy/wiki