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

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

強いスキップを有効にする

まず、強力なスキップモードを有効にしてみてください。強いスキップモードでは、不安定なパラメータを持つコンポーザブルをスキップできます。これは、安定性によって発生するパフォーマンスの問題を修正する最も簡単な方法です。

詳しくは、強いスキップをご覧ください。

クラスを不変にする

不安定なクラスを完全に不変にすることもできます。

  • Immutable: 型のインスタンスの作成後にプロパティの値が変更されることがなく、すべてのメソッドが参照透過である型を示します。
    • クラスのすべてのプロパティが var ではなく val であり、不変型であることを確認します。
    • String, IntFloat などのプリミティブ型は常に不変です。
    • これが不可能な場合は、変更可能なプロパティに Compose の状態を使用する必要があります。
  • Stable: 可変型を示します。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 my datalayer stable
com.datalayer.*
// 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 コンパイラ Gradle プラグイン構成の composeCompiler オプション ブロックに渡します。

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

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

複数のモジュール

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

データレイヤが UI レイヤとは別のモジュールにある場合(おすすめの方法)、これが問題が発生する可能性があります。

解決策

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

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

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

すべてのコンポーザブルをスキップ可能にする必要はない

安定性に関する問題を修正する際に、すべてのコンポーズ可能な要素をスキップできるようにすることはおすすめしません。これを試みると、早期最適化につながり、修正するよりも多くの問題が発生する可能性があります。

スキップ可能であることに実質的なメリットがなく、コードの保守が困難になる場合も多くあります。例:

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

コンポーザブルがスキップ可能の場合、多少のオーバーヘッドが発生します。再実行可能にすることに価値があると判断した場合には、コンポーザブルに「再起動不可」というアノテーションを付けることもできます。