EpoxyでRecyclerViewをもっと簡単にする

kazutoyo
PicApp Tech Blog
Published in
11 min readDec 15, 2018

--

この投稿はピックアップ Advent Calendar 2018の15日目です。

こちらに投稿するのは『ML Kitを試してみる』以来ということで、半年以上ぶりですね笑

さて、最近私が担当していますチャット型小説アプリのTELLERのAndroid版でトップ画面のUIの変更を行いました。

Android v3.3のトップ画面

以前は単純なリスト + ヘッダーにカルーセルが1つという構成でだったのですが、今回はカード型のカルーセルやランキングなど、リスト内に複数のViewのタイプが存在するようになっています。

RecyclerViewをお使いの方はよく分かるかと思いますが、RecyclerViewのAdapterで複数のViewTypeを使おうとすると、positionから表示するViewTypeを切り分けると言った処理が必要で、その処理が若干わかりづらかったり、面倒だったりしました。

今回のようにいくつもViewTypeが増えてくるとその処理が複雑化する恐れがありそうだったのでなにかいい方法はないかと探すと、airbnbが公開しているEpoxyが良さそうでした。

Epoxyのメリットとして

  • 複数のViewTypeの扱いが楽になる
  • Epoxyの差分更新があるためパフォーマンスも良い
  • DataBindingやPagingLibraryなどもサポートしているので、既存の実装から大きく変える必要はない
  • Kotlinもサポートしている

などがありました。

既存のTELLERではDataBindingやPagingLibraryを使っていましたが、そのあたりもサポートされていたので今回試してみることにしました。

Epoxyのセットアップ

まずapp/build.gradleにEpoxyを追加しておきます。(DataBinding, PagingLibraryはお好みで)

Kotlinを使うときはcorrectErrorTypes = trueとしておきましょう

dependencies {
implementation 'com.airbnb.android:epoxy:3.1.0'
// DataBinding Support
implementation 'com.airbnb.android:epoxy-databinding:3.1.0'
// Paging Library Support
implementation 'com.airbnb.android:epoxy-paging:3.1.0'
annotationProcessor 'com.airbnb.android:epoxy-processor:3.x.y'
}

// Kotlin
kapt {
correctErrorTypes = true
}

Epoxyを使う

EpoxyはModelとControllerという2つの要素をつくることでRecyclerViewのAdapterをつくります。

Modelは各Viewの要素、ControllerはそのModelをどう表示するかを制御するためのものというものです。

Modelの作り方はCustomView/DataBinding/ViewHolderをサポートしています。

CustomViewでModelを作る場合

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)

とアノテーションを追加します。

また、Modelに値を渡す場合、

@ModelProp
fun setItem(item: Item) {
// ..
}

と、セッターメソッドに@ModelPropアノテーションを追加します。

テキストをセットするのに適した @TextProp など、@ModelPropsの代わりに使うアノテーションも良いされています。 詳しくは公式のWikiを見ると良いでしょう。

DataBindingでModelを作る場合

アプリケーションのパッケージのルートの package-info.java にレイアウトを個別で指定するか、マッチするパターンを記述するとModelを生成してくれます。

@EpoxyDataBindingPattern(rClass = R.class, layoutPrefix = "list_item")
@EpoxyDataBindingLayouts({
R.layout.view_feed_loading_error
})
package jp.picappinc.teller;
import com.airbnb.epoxy.EpoxyDataBindingLayouts;

また、DataBindingのModelをカスタマイズしたい場合、DataBindingEpoxyModelを継承し、 @EpoxyModelClass アノテーションを追加することでカスタマイズされたModelが生成されます。 このモデルに値を渡したい場合、 @EpoxyAttribute アノテーションをプロパティに追加します。

@EpoxyModelClass(layout = R.layout.list_header)
abstract class ListHeaderModel : DataBindingEpoxyModel() {
@EpoxyAttribute
var sectionTitle: String? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var clickListener: OnClickListener? = null
override fun setDataBindingVariables(binding: ViewDataBinding?) {
binding?.setVariable(BR.sectionTitle, sectionTitle)
binding?.setVariable(clickListener, clickListener)
}
}

Controllerを実装する

さて、ここまでできたModelをControllerに追加し、表示できるようにします。

EpoxyControllerを継承し、buildModelsメソッドを実装します。

例えばリストとフッターにローディングの読み込みをするようなRecyclerViewでは、次のようにbuildModels内でEpoxyControllerにModelを追加していきます。

Modelの追加はaddToまたはaddIf(addToの条件付き)で行います。

また、buildModelsはrequestModelBuild()が呼ばれたタイミングで行われるため、プロパティが変更されたときに呼ぶと良いでしょう。

class ListItemController : EpoxyController() {
var items: List<Item> = emptyList()
set(value) {
field = value
requestModelBuild()
}

var showLoading: Boolean = false
set(value) {
field = value
requestModelBuild()
}

override fun buildModels() {
items.forEach { item ->
ListItemModel_()
.id(item.id)
.item(item)
.addTo(this)
}

ListLoadingViewBindingModel_()
.id("loading")
.addIf(showLoading, this)
}
}

KotlinのExtensionで次のような書き方も可能です。

class ListItemController : EpoxyController() {
var items: List<Item> = emptyList()
set(value) {
field = value
requestModelBuild()
}

var showLoading: Boolean = false
set(value) {
field = value
requestModelBuild()
}

override fun buildModels() {
items.forEach { item ->
listItem {
id(item.id)
item(item)
}
}

if (showLoading) {
listLoadingView {
id("loading")
}
}
}
}

PagingLibrary Support

PagingLibraryを使う場合、PagedListEpoxyControllerを使います。

buildItemModelメソッド内でPagedListのModelを生成します。

またそれ以外の値を追加したい場合、addModelsメソッド内でList内に追加を行います。

class ItemListController: PagedListEpoxyController<ContentItemModel>() {

var showLoading: Boolean = false
set(value) {
field = value
requestModelBuild()
}

override fun buildItemModel(currentPosition: Int, item: ContentItemModel?): EpoxyModel<*> {
val item = requireNotNull(item)
return ContentItemListComponentModel_()
.id(item.id)
.item(item)
}

override fun addModels(models: List<com.airbnb.epoxy.EpoxyModel<*>>) {
val newModels = models.toMutableList()
if (showLoading) {
newModels.add(ListLoadingViewBindingModel_().id("loading"))
}

super.addModels(newModels)
}
}

RecyclerViewに追加する

さて、これでEpoxyを使うためのModelとControllerをつくることができました!

Controllerの getAdapter()メソッドでRecyclerView.Adapterを取得できるため、これをRecyclerViewにセットするだけです。

recyclerView.adapter = controller.adapter

これでEpoxyでRecyclerViewのAdapterをつくることができました!

Carousel

EpoxyのCarouselを使うことにより、このようなCarouselを簡単に実装することが出来ます。

アイテムが画面内に表示される個数の指定やPaddingなど、簡単に指定することができ、簡単にカルーセルの実装を行うことが出来ます。

override fun buildModels() {
if (items.isNotEmpty()) {
val itemModels = items.map { item ->
ListItemBindingModel_()
.id(item.id)
.item(item)
}

CarouselModel_()
.id("carousel")
.paddingDp(4)
.numViewsToShowOnScreen(3.5f)
.models(itemModels)
.addTo(this)
}
}
}

この他にもGridやドラッグ、スワイプの機能など、RecyclerViewで行いたいことはほぼサポートされているかと思います。

自分の中ではもうEpoxyなしではRecyclerViewは考えられない!ってくらい便利なので、これからも使っていこうと思います!

以上、ピックアップ Advent Calendar 2018の15日目でした。

あと1,2投稿ほどiOSとAndroidについて書こうかなって思うのでよろしくおねがいします!

そして働く仲間もお待ちしております!こちらもよろしくおねがいします!

https://dmm-corp.com/recruit/engineer/5552

--

--