LCL Engineers' Blog

バス比較なび・格安移動・バスとりっぷを運営する LCLの開発者ブログ

EpoxyでRecyclerView in RecyclerView を簡単に実現する

バックエンド兼Androidアプリエンジニアの高橋です。

久しぶりの更新となってしまいました。

というのも、バスツアー検索サイトのバックエンド開発をこの数ヶ月間ずっと担当していて、つい先月ようやくサイト公開の運びとなりました。👏 (宣伝)

tour.bushikaku.net

開発工程や使っている技術スタックについては、また別の機会に当ブログで紹介していきたいと思います。

今回のテーマはそれと全く関係ないAndroidの話です。

RecyclerView in RecyclerViewをEpoxyというライブラリを使って簡単に実現してみました。

Epoxyとは

ざっくり言うと、Airbnb が開発しているRecyclerViewを簡単に実装するためのライブラリです。

github.com

RecyclerViewというと、Adapterクラスを用意してitemのpositionによってViewを切り替えたり、いわゆるボイラープレート的な実装が多くてAndroidアプリ開発者にとっては少し距離を置きたくなる(?)存在でしたが、 Epoxyを使うとAdapterクラスが不要になります。

似たようなライブラリにGroupieがあり、実装自体はこちらの方が楽?な印象ですが、Epoxyの方が開発が盛んで拡張性もあるので今回採用しました。

Epoxyの導入方法や基本的な使い方はググればそれなりに出てくるので今回は割愛します。

今回はRecyclerViewの中にRecyclerViewをネストするやり方が調べてもあまり出てこなかったので、そのやり方の一例をご紹介します。

実現したい構成

ざっくりこんな構成のものを作ってみます。

f:id:ktx_ku:20191001142110p:plain

一番外側をRecyclerViewで作り、その各itemの中にも子RecyclerViewが内包されている構成です。

実際の画面はこんな感じになります。(ちょっとわかりづらいですが。)

f:id:ktx_ku:20191001184936p:plain

緑のセクションが外側のRecyclerViewのitemで、オレンジのGridに並んでいるitemが内側のRecyclerViewのitemです。

Epoxyを使った例

上記の図をEpoxyで実現する方法ですが、実は公式ドキュメントにはネストするRecyclerViewの作り方はあまり詳細に載っていません。

Carousel を使う方法もあるようですが、より複雑なViewを実現するためには不向きな感じだったので独自の方法でやってみました。

Carouselで要件を満たせるようなら基本的にはCarouselを使った方がいいと思います。

また、今回は以下のものを利用した例となります。

レイアウトを作成

まず、画面全体をEpoxyRecyclerViewで作ります。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.airbnb.epoxy.EpoxyRecyclerView
        android:id="@+id/parentRecycler"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="vertical"/>

</LinearLayout>

次に 外側のRecyclerViewのitemです。

childRecyclerというidを振ったところがGrid形式で表示する内側の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">
    <data>
        <variable
            name="text"
            type="String"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#7CFC00"
        android:orientation="vertical">

        <TextView
            android:id="@+id/sectionCountText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{text}"/>

        <com.airbnb.epoxy.EpoxyRecyclerView
            android:id="@+id/childRecycler"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="4dp"
            android:orientation="vertical"
            app:spanCount="5"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
    </LinearLayout>
</layout>

最後に内側のRecyclerViewのitemです。

ただのTextViewですね。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="text"
            type="String"/>
    </data>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FFBA5C"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/sectionCountText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{text}"/>
    </LinearLayout>
</layout>

コード

上記のxmlができたら、一回ビルドしてEpoxyModelクラスを自動生成し、それらを使ってRecyclerViewを構築していきます。

以下、該当するところの抜粋です。(今回はFragment上に作っています。)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // RecyclerViewを構築
        buildRecyclerView()
    }

    // 外側のRecyclerViewを構築
    private fun buildRecyclerView() {
        parentRecycler.withModels {
            // 20セクション分作る
            repeat(20) { i ->
                val count = i + 1
                itemParentRecycler {
                    id("parent_${count}")
                    text("${count}つ目のセクション")
                    onBind { model, view, position ->
                        // onBind時に内側のRecyclerViewを構築
                        buildChildRecyclerView(view.dataBinding.root.childRecycler, position)
                    }
                }
            }
        }
    }

    // 内側のRecyclerViewを構築
    private fun buildChildRecyclerView(childRecycler: EpoxyRecyclerView, parentPosition: Int) {
        childRecycler.withModels {
           // 1セクション10item作る
            repeat(10) { i ->
                val count = i + 1
                itemChildRecycler {
                    id("child_${count}_parent_${parentPosition}")
                    spanCount = 5
                    text("${count}つ目のitem")
                }
            }
        }
    }

かなり説明省いてしまいましたが、Adapterクラスが全く出てこず、コード量もかなり少ないのがわかるかと思います。

従来の方法でやろうとするとAdapterクラスが2つ必要になる上、ボイラープレートもたくさん書かなくてはいけないですが、Epoxyを使うことでそのストレスから解放されました。

Kotlin SupportによってFragmentクラス上で全て完結するのもスッキリしていいですね。

まとめ

今回、Epoxyの概要とその実例として、RecyclerView in RecyclerViewの構成が簡単に作れることをご紹介しました。

例に挙げたような簡単な画面構成であれば正直Epoxyを使うまでもないかもしれませんが、Viewの種類が増えてきたり、RecyclerViewのネストが複雑になってくるとメンテナンスが辛くなってくるので、最初からEpoxyを使うことを是非お勧めします。

Epoxyは開発も盛んに行われているようなので安心して導入できるかと思います。

最後に、LCLではメンテナンス性の高いコードを考えることが好き、得意と言う人を募集中です!

少しでも興味を持っていただけたら、カジュアル面談からでもご応募お待ちしています!

www.lclco.com