安定性に関する問題を解決する

パフォーマンスの問題を引き起こす不安定なクラスがある場合は、安定したクラスにする必要があります。このドキュメントでは、そのために使用できるいくつかの手法について説明します。

クラスを不変にする

まず、不安定なクラスを完全に不変にする必要があります。

  • 不変: その型のインスタンスの構築後にプロパティの値が変更できない型であり、すべてのメソッドが参照透過的であることを示します。
    • クラスのすべてのプロパティが var ではなく val であることと、不変型のプロパティであることを確認します。
    • String, IntFloat などのプリミティブ型は常に不変です。
    • これが不可能な場合は、可変プロパティに Compose の状態を使用する必要があります。
  • 安定: 可変型を示します。Compose ランタイムは、型のパブリック プロパティまたはメソッドの動作のいずれかが以前の呼び出しと異なる結果をもたらすかどうか、またどのような場合にそれを認識しません。

不変のコレクション

Compose がクラスが不安定であると見なされる一般的な理由は、コレクションです。安定性の問題を診断するのページで説明したように、Compose コンパイラは、List, MapSet などのコレクションが真に不変であることを完全には確認できないため、不安定版としてマークされます。

この問題を解決するには、不変コレクションを使用します。Compose コンパイラは、Kotlinx 不変コレクションをサポートしています。こうしたコレクションは不変であることが保証されており、Compose コンパイラはそのように扱います。このライブラリはまだアルファ版であるため、API が変更される可能性がございます。

安定性の問題の診断ガイドにある不安定なクラスについても再度検討してください。

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

不変コレクションを使用すると、tags を安定させることができます。クラスで、tags の型を ImmutableSet<String> に変更します。

data class Snack{
    …
    val tags: ImmutableSet<String> = persistentSetOf()
    …
}

その後、クラスのすべてのパラメータは不変になり、Compose コンパイラはそのクラスを安定版としてマークします。

Stable または Immutable でアノテーションを付ける

安定性の問題を解決する方法としては、不安定なクラスに @Stable または @Immutable でアノテーションを付ける方法があります。

クラスにアノテーションを付けると、コンパイラがクラスについて推測するものがオーバーライドされます。これは、Kotlin の !! 演算子に似ています。これらのアノテーションの使用方法には細心の注意を払ってください。コンパイラの動作をオーバーライドすると、コンポーザブルが想定どおりに再コンポーズしないなど、予期しないバグが発生する可能性があります。

アノテーションなしでクラスを安定させることができる場合は、その方法で安定性を得るようにしてください。

次のスニペットは、不変としてアノテーションが付けられたデータクラスの最小限の例を示しています。

@Immutable
data class Snack(
…
)

@Immutable アノテーションと @Stable アノテーションのどちらを使用する場合でも、Compose コンパイラは Snack クラスを安定版としてマークします。

コレクション内のアノテーション付きのクラス

List<Snack> 型のパラメータを含むコンポーザブルについて考えてみましょう。

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  …
  unstable snacks: List<Snack>
  …
)

Snack@Immutable アノテーションを付けても、Compose コンパイラは HighlightedSnackssnacks パラメータを不安定要素としてマークします。

コレクション型に関しては、パラメータがクラスと同じ問題に直面します。Compose コンパイラは、List 型のパラメータを常に不安定としてマークします。これは、安定した型のコレクションであっても関係します。

個々のパラメータを安定版としてマークすることはできません。また、コンポーザブルにアノテーションを付けて、常にスキップ可能にすることもできません。今後の移行には複数の方法があります。

不安定なコレクションの問題を回避するには、いくつかの方法があります。以降のサブセクションで、これらのさまざまなアプローチの概要を説明します。

構成ファイル

コードベースの安定性に関する規約に準拠する場合は、安定性構成ファイルkotlin.collections.* を追加することで、Kotlin コレクションを安定版と見なすことができます。

不変コレクション

不変性をコンパイル時の安全性を確保するために、List の代わりに kotlinx の不変コレクションを使用できます。

@Composable
private fun HighlightedSnacks(
    …
    snacks: ImmutableList<Snack>,
    …
)

Wrapper

不変コレクションを使用できない場合は、独自のコレクションを作成できます。そのためには、アノテーション付きの安定版クラスで List をラップします。要件によっては、汎用ラッパーがおそらく最適な選択です。

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

これをコンポーザブルのパラメータの型として使用できます。

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

解決策

いずれかのアプローチを取ると、Compose コンパイラは HighlightedSnacks コンポーザブルを skippablerestartable の両方としてマークするようになりました。

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

再コンポーズの際に、入力に変更がない場合に Compose が HighlightedSnacks をスキップできるようになりました。

安定性構成ファイル

Compose Compiler 1.5.5 以降では、安定版とみなされるクラスの構成ファイルをコンパイル時に提供できるようになりました。これにより、LocalDateTime のような標準ライブラリ クラスなど、制御していないクラスを安定版と見なすことができます。

構成ファイルは、1 行に 1 つのクラスがある書式なしテキスト ファイルです。コメント、単一ワイルドカード、二重ワイルドカードがサポートされています。構成例を以下に示します。

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

この機能を有効にするには、構成ファイルのパスを Compose コンパイラ オプションに渡します。

Groovy

kotlinOptions {
    freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
                    project.absolutePath + "/compose_compiler_config.conf"
    ]
}

Kotlin

kotlinOptions {
  freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
  )
}

Compose コンパイラはプロジェクト内の各モジュールで個別に実行されるため、必要に応じてモジュールごとに異なる構成を指定できます。または、プロジェクトのルートレベルに構成を 1 つ作成し、そのパスを各モジュールに渡します。

複数のモジュール

もう一つの一般的な問題は、マルチモジュール アーキテクチャです。Compose コンパイラは、参照する非プリミティブ型がすべて安定版として明示的にマークされているか、Compose コンパイラでビルドされたモジュール内にある場合にのみ、クラスが安定しているかどうかを推測できます。

データレイヤが UI レイヤとは別のモジュールにある場合(推奨されるアプローチ)、これは問題になる可能性があります。

解決策

この問題を解決するには、次のいずれかの方法を使用できます。

  1. クラスをコンパイラ構成ファイルに追加します。
  2. データレイヤー モジュールで Compose コンパイラを有効にするか、必要に応じて @Stable または @Immutable をクラスにタグ付けします。
    • そのためには、Compose の依存関係をデータレイヤーに追加します。ただし、これは単に Compose ランタイムの依存関係であり、Compose-UI に対する依存関係ではありません。
  3. UI モジュール内で、データレイヤー クラスを UI 固有のラッパークラスでラップします。

Compose コンパイラを使用していない外部ライブラリを使用する場合も、同じ問題が発生します。

すべてのコンポーザブルをスキップ可能にする必要があるわけではありません

安定性の問題を解決する際には、すべてのコンポーザブルをスキップ可能にする必要はありません。自分で最適化しようとすると、最適化が早まり、修正以上の問題が生じる可能性があります。

スキップ可能にしてもメリットがなく、コードの保守が困難になるケースは数多くあります。次に例を示します。

  • 頻繁に再コンポーズされないコンポーザブル、またはまったく再コンポーズされないコンポーザブル。
  • スキップ可能なコンポーザブルを呼び出すだけのコンポーザブル。
  • パラメータの数が多いコンポーザブルで、高価な実装が等しい場合。この場合、パラメータが変更されたかどうかを確認するコストが、安価な再コンポーズのコストを上回る可能性があります。

コンポーザブルがスキップ可能になっていると、わずかなオーバーヘッドが増加しますが、その価値はありません。再起動可能にするとオーバーヘッドが価値より大きくなると判断した場合は、コンポーザブルに再起動不可のアノテーションを付けることもできます。