👋

Compose Stable Markerを使ってCompose runtimeに依存しないモジュールのクラスを安定にする

2023/10/02に公開

この記事ではskydovesさんが公開しているCompose Stable Markerというライブラリを用いてCompose runtimeに依存しないモジュールのクラスを安定にする方法を紹介します。

Jetpack Composeにおける安定性(Stability in Compose)

Compose CompilerはComposable Functionsのパラメータの型・クラスをstable(安定)もしくはunstable(不安定)としてマークします。安定なパラメータは不変であるかもしくはComposeのrecompositionの間にその値が変更されたかどうかを検知できるもの、対照的に不安定なパラメータはComposeのrecompositionの間にその値が変更されたかどうかを検知できないものを表します。

Composable Functionsのパラメータの安定性はパフォーマンスの観点で重要です。パラメータが全て安定なComposable Functionsは、コンパイラにSkippableとみなされ親のComposableがrecomposeされても、Composableのパラメータの値が不変であれば不必要なrecomposeをスキップします。一方、パラメータに不安定なものが一つでも存在すればSkippableとはならず、親のComposableがrecomposeされた場合、パラメータの値が不変であっても合わせてrecomposeされてしまいます。

また不安定なクラスを安定なクラスとしてマークさせることも可能です。具体的な手段は状況によりますが、Compose runtimeに用意されている@Stableまたは@Immutableを不安定なクラスへ適用することが手軽です。

マルチモジュールのプロジェクトにおける安定性の注意点

マルチモジュールプロジェクトでは、Jetpack Composeの安定性に関しての注意点があります。Compose Compilerは参照される非プリミティブ型が全て安定とマークされ、かつCompose Compilerによってビルドされたモジュール内に存在する場合にのみクラスを安定とみなします。これは、Compose runtimeに依存していないData LayerやDomain Layerのモジュールのクラスは、デフォルトで不安定とみなされることを意味します。

公式のFix stability issuesでは次のような解決策があげられています。

  1. 対象のモジュールでCompose runtimeへの依存を追加する
  2. UI Layerで他のモジュールのクラスをラップするクラスをつくる

しかし、UI以外のレイヤーがCompose runtimeに依存することや、UI Layerでのクラスの扱いが冗長になることが気になる方もいるでしょう。

// UI Layer以外のモジュール
dependencies {
    // 依存関係が広くなる
    implementation "androidx.compose.runtime:runtime:<version>"
}
@Immutable
data class ImmutableWrapper<T>(
    // 値の参照が冗長になる
    val value: T
)

Compose Stable Markerを試す

そこで今回はCompose runtimeに依存させずに@Stable/@Immutableを利用することができるCompose Stable Markerというライブラリを試してみます。

Compose Stable Markerの実体

最初に簡単にCompose Stable Markerの実体について確認します。Compose Stable MarkerはCompose runtimeの安定性に関連する3つのアノテーション、@Stable/@Immutable/@StableMarkerをバックポートしています。特徴として、androidx.compose.runtimeをパッケージ名にもちます。

Compose Stable MarkerとCompose runtimeのアノテーションの完全修飾名は一致しています。結果、Compose Stable MarkerのアノテーションもCompose Compilerに捕捉されます。

前提

早速、Compose Stable Markerを試していきましょう。説明のために

  • Compose runtineに依存しないモジュールに定義したBookクラス
  • BookクラスをパラメータにとるBookComposable

を用意します。

Compose runtineに依存しないモジュールに定義したBookクラス
data class Book(
    val title: String,
    val author: String,
)
BookクラスをパラメータにとるBookComposable
@Composable
fun BookComposable(book: Book) {
  Text(text = book.title)
}

そしてCompose Compiler Metricsを用いてBookComposableを確認します。

@Immutable付与前のCompose Compiler Metricsの出力
restartable scheme("[androidx.compose.ui.UiComposable]") fun BookComposable(
  unstable book: Book
)

Bookクラスはunstableとしてマークされており、BookComposableもskippableでないのが確認できます。

Gradleの設定

Gradleの設定を行います。
安定にしたいクラスが定義されているモジュールのgradleに依存関係を設定しましょう。

Compose Stable Markerは@Stable/@Immutableアノテーションで構成されるライブラリでありランタイムでは必要とされません。
そのため、依存関係としてimplementationではなくcompileOnlyを使います。

dependencies {
    compileOnly("com.github.skydoves:compose-stable-marker:1.0.0")
}

アノテーションの適用

次にCompose Stable Markerに定義されているアノテーションを実際に適用してみます。今回、Bookクラスに@Immutableを付与します。Compose runtimeで定義されている@Stable/@Immutableと同じ要領でアノテーションを使いわけましょう。

// 1. androidx.compose.runtime.Immutableをimport
import androidx.compose.runtime.Immutable

// 2. stableとしてマークさせたいクラスにアノテーションを付与
@Immutable
data class Book(
  val title: String,
  val author: String,
)

先述の通り、Compose Stable Markerはパッケージ名もCompose runtimeで定義されているアノテーションと共通です。そのため、importandroidx.compose.runtimeが登場します。

安定性の確認

最後に安定性の確認を行いましょう。先ほどと同様にBookComposableを確認します。

@Composable
fun BookComposable(book: Book) {
  Text(text = book.title)
}
@Immutable付与後のCompose Compiler Metricsの出力
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun BookComposable(
  stable book: Book
)

BookComposableのBookはstableとしてマークされ、同時にBookComposableがskippableになったのが確認できます。

まとめ

skydovesさんが公開しているCompose Stable Markerというライブラリを用いてCompose runtimeに依存しないモジュールのクラスを安定にする方法を紹介しました。アノテーションの完全修飾名を一致させるアプローチは賢いなと感じました。

不安定なクラスを安定にさせる方法としてはImmutable collectionsと合わせて利用することでほとんどのケースに対応できる印象です。Jetpack ComposeはXMLベースのAndroid Viewに比べてパフォーマンスに対する配慮がしばしば必要になることが多いので安定性に関してもできるだけ気に掛けることができると良さそうです。

参考文献

Discussion