🛖

EpoxyRecyclerViewの一部にComposeを使用する

2023/02/28に公開

初めに

ポートでAndroid開発をしている @shxun6934 です。

今回は、com.airbnb.android:epoxy-composeを使用して、EpoxyRecyclerViewの一部にJetpackCompose(以下Composeと呼ぶ)を使用する方法を書きたいと思います。

Epoxyとは

Airbnbが提供している、RecyclerViewを簡単に構築することができるAndroidライブラリです。

https://github.com/airbnb/epoxy

RecyclerViewについては、Google公式のドキュメントがあるので、そちらをみてください。

https://developer.android.com/guide/topics/ui/layout/recyclerview

RecyclerViewを構築する際は、AdapterViewHolderを実装する必要がありますが、Epoxyがそれらの代わりに実装をしてくれるので、簡単に実装ができます。
代わりに実装者は、EpoxyModelEpoxyControllerを実装して、RecyclerViewの制御を行います。

Epoxyは、data-bindingにも対応をしていて、専用のアノテーション(@EpoxyDataBindingLayouts)を使用しているとEpoxyModelとEpoxyControllerも自動で生成してくれるので、もっと簡単に実装できます。

詳しい内容は、GithubのWikiに記載されているので、参考にしてください。

https://github.com/airbnb/epoxy/wiki/Data-Binding-Support
https://github.com/airbnb/epoxy/wiki/EpoxyRecyclerView#kotlin-extensions
https://github.com/airbnb/epoxy/wiki/Epoxy-Controller#usage-with-kotlin

Jetpack Composeとは

GoogleI/O 2019で発表された、AndroidのUIを宣言的に構築できるツールキットです。

https://developer.android.com/jetpack/compose

宣言的UIとは何かについては、以下の記事が個人的に参考になりました。

https://zenn.dev/arei/articles/f59e263aa3edf2

EpoxyRecyclerViewをComposeに置き換える

ここからが本題です。

弊社では、xml・data-bindingで構成されている既存のUIをComposeに置き換える対応を行っています。
既存のUIでリスト表示やViewを構築する際に、data-bindingでのEpoxyRecyclerViewを使用しています。

ComposeにもLazyRowLazyColumnといった、多数のアイテムを表示するコンポーネントが提供されているので、簡単なリスト表示であれば、そちらを使用しますが、EpoxyRecyclerViewで実装しているUIは結構複雑なUIだったので、LazyRowやLazyColumnに置き換えるのに時間がかかりました。
そこでちょうど、Epoxyの5.0.0からComposeに対応した(安定版) ので、そちらを使用してEpoxyRecyclerViewの一部のUIをComposeで実装してみようと思いました。

AndroidViewのようにxmlやdata-bindingと併用してComposeをかけるので、とても便利で実装しやすかったです。
その書き方について簡単に紹介します。

今回は、data-bindingも使用していることとComposeをAndroidプロジェクトに導入していることを前提で書きたいと思います。

導入

build.gradlecom.airbnb.android:epoxy-composeを書いて、プロジェクトに読み込みさせます。

marven repositoryにバージョンが記載されているので、バージョンを調べて入れてください。
https://mvnrepository.com/artifact/com.airbnb.android/epoxy-compose

build.gradle
apply plugin: "kotlin-kapt"

android {

    // 中略
    
    buildFeatures {
        dataBinding true  // data-binding有効
        compose true      // compose有効
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.2"
    }
}

kapt {
    correctErrorTypes = true
}

dependencies {

    // 中略

    // compose
    def jetpack_compose_version = "2023.01.00"
    implementation platform("androidx.compose:compose-bom:$jetpack_compose_version")
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.material:material"

    // epoxy
    def epoxy_version = "5.1.1"
    implementation "com.airbnb.android:epoxy:$epoxy_version"
    implementation "com.airbnb.android:epoxy-databinding:$epoxy_version"  // epoxy with data-binding
    implementation "com.airbnb.android:epoxy-compose:$epoxy_version"      // epoxy with compose
    kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
}

レイアウト

EpoxyのComposeは、EpoxyRecyclerView上で動作するので、レイアウトファイルが必要です。
(ここまでは、普通にEpoxyRecyclerViewを使用するのと一緒です。)

activity_main.xml
<!-- data-bindingなので、layoutタグで囲む -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <com.airbnb.epoxy.EpoxyRecyclerView
        android:id="@+id/epoxy_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</layout>

Activity・Fragmentにbindingを紐付ける

data-bindingを使用しているので、一度ビルドすると、ActivityMainBindingのようなViewDataBindingを継承したクラスが生成されると思います。これをActivity・Fragmentと紐付けます。

MainActivity.kt
class MainActivity: AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // bindingにレイアウトを読み込ませる
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    }
}

EpoxyRecyclerView内にJetpackComposeをおく

withModelsメソッドのラムダ内で、composableInteropメソッドを呼び出します。
そのラムダ内でコンポーネントを呼び出すと、Composeで作成されたUIがEpoxyRecyclerView常に表示されます。

MainActivity.kt
class MainActivity: AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // bindingにレイアウトを読み込ませる
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        
        // EpoxyRecyclerViewのBind
        binding.epoxyRecyclerView.withModels {

            // Composeのコンポーネントを呼び出す
            composableInterop("hello_text") {
                HelloText("Hello, World!")
            }
        }
    }
    
    @Composable
    private fun HelloText(text: String) {
        Text(
            text = text,
            color = Color.Red,
            fontSize = 32.sp,
            fontWeight = FontWeight.Bold
        )
    }
}

実行

実際に実行すると以下のようになります!

最後に

data-bindingとComposeを併用する場合でも、難しい設定やコードを書かずに実装をすることができます。
UIをComposeで書く分、アニメーションや状態管理はしやすいかなと個人的に思います。

Epoxyを使用しているプロジェクトにComposeを導入することを考えている場合は、併用するということも視野に入れてみるといいと思います!
その際は、この記事を参考にしていただけると幸いです。🙇

サンプルコード

今回のサンプルコードをGithubにあげているので、コードを見たい方はこちら。
https://github.com/shxun6934/EpoxyWithComposeSample

公式もサンプルコードを提供しているので、そちらも参考にしてみてください!
https://github.com/airbnb/epoxy/blob/402404f39057fa8faee3a8214653f15573d85c99/epoxy-composesample/src/main/java/com/airbnb/epoxy/compose/sample/ComposableInteropActivity.kt

Discussion