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

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

強力なスキップを有効にする

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

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

クラスを不変にする

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

  • 不変: その型のインスタンスが作成された後に、プロパティの値が変更されることがなく、すべてのメソッドが参照的に透過的である型を示します。
    • クラスのすべてのプロパティが 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 つ作成し、そのパスを各モジュールに渡します。

複数のモジュール

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

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

解決策

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

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

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

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

安定性の問題を解決する際は、すべてのコンポーザブルをスキップ可能にすることはできません。これを行おうとすると最適化が早まり、修正するよりも多くの問題が発生する可能性があります。

スキップ可能であっても実質的なメリットがなく、コードの保守が困難になる状況はよくあります。次に例を示します。

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

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