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

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

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

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

詳細については、強力なスキップをご覧ください。

クラスを不変にする

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

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

変更不可のコレクション

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

この問題を解決するには、不変コレクションを使用します。Compose コンパイラには、Kotlinx Immutable Collections のサポートが含まれています。これらのコレクションは変更不能であることが保証されており、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 でアノテーションを付ける

安定性に関する問題を解決する 1 つの方法は、不安定なクラスに @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 コンパイラ 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 つの構成を用意し、そのパスを各モジュールに渡します。

複数のモジュール

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

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

解決策

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

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

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

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

安定性の問題を修正する際は、すべてのコンポーザブルをスキップ可能にしようとしないでください。早計な最適化は、修正するよりも多くの問題を引き起こす可能性があります。

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

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

コンポーザブルをスキップ可能にすると、わずかなオーバーヘッドが発生しますが、それに見合う価値がない場合があります。再起動可能にすることがメリットよりもオーバーヘッドが大きいと判断した場合は、コンポーザブルに non-restartable のアノテーションを付けることもできます。