強力なスキップモード

強いスキップは、Compose コンパイラで使用できるモードです。有効にすると、コンパイラの動作が次の 2 つの方法で変更されます。

  • 不安定なパラメータを持つコンポーザブルがスキップ可能になる
  • 不安定なキャプチャを含むラムダは remember される

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

Kotlin 2.0.20 では、強力なスキップがデフォルトで有効になっています。

2.0.20 より前のリリースで Gradle モジュールの強力なスキップを有効にするには、Gradle 構成の composeCompiler ブロックに次のオプションを含めます。

android { ... }

composeCompiler {
   enableStrongSkippingMode = true
}

コンポーザブルのスキップ可能性

強力なスキップモードでは、スキップとコンポーズ可能な関数に関して、Compose コンパイラによって通常適用される安定性ルールの一部が緩和されます。デフォルトでは、Compose コンパイラは、すべての引数に安定した値がある場合、コンポーズ可能な関数をスキップ可能としてマークします。強いスキップモードでは、この動作が変更されます。

強いスキップを有効にすると、再開可能なコンポーズ可能な関数はすべてスキップ可能になります。これは、不安定なパラメータがあるかどうかに関係なく適用されます。再起動不可能なコンポーズ可能な関数は、スキップ不可のままです。

スキップするタイミング

再コンポジション中にコンポーザブルをスキップするかどうかを判断するために、Compose は各パラメータの値を以前の値と比較します。比較のタイプは、パラメータの安定性によって異なります。

  • 不安定なパラメータは、インスタンスの等価性(===)を使用して比較されます。
  • 安定したパラメータは、オブジェクトの等価性(Object.equals())を使用して比較されます。

すべてのパラメータがこれらの要件を満たしている場合、Compose は再コンポーズ中にコンポーザブルをスキップします。

コンポーザブルで強力なスキップを無効にしたい場合があります。つまり、再起動可能だがスキップ不可のコンポーザブルが必要になる場合があります。この場合は、@NonSkippableComposable アノテーションを使用します。

@NonSkippableComposable
@Composable
fun MyNonSkippableComposable {}

クラスに安定版としてアノテーションを付ける

インスタンスの等価性(===)ではなく、オブジェクトの等価性(Object.equals())を使用してオブジェクトを比較する場合は、引き続き @Stable または @Immutable を使用して、指定されたクラスにアノテーションを付けます。

@Stable または @Immutable を追加する前に、次のトレードオフを考慮してください。

  • Compose モジュール内の安定したプロパティを持つクラスはすでに安定している: Compose コンパイラでコンパイルされたモジュール内の安定した型の val プロパティを持つクラスは、自動的に安定していると推測され、@Immutable や @Stable を必要とせずに .equals() を使用します。
  • .equals() チェックが低コストのクラスにのみアノテーションを付ける: コンパイラが安定性を推測できず(Compose 以外のモジュールのクラスなど)、同じ値を持つ新しいインスタンスが頻繁に割り当てられる場合は、クラスの .equals() チェックが低コストであれば、@Stable または @Immutable を追加します。
  • コレクションを含むモデルにアノテーションを付ける場合は注意する: コレクションを含むモデルに @Stable または @Immutable を追加する(または List を ImmutableList に置き換える)と、Compose は再コンポーズ パスごとにコレクション内のすべての要素を .equals() と比較します。遅延レイアウト(LazyColumn、LazyRow、LazyGrid)で使用される大規模なコレクションやモデルの場合、すべての要素を比較すると、インスタンスの等価性(===)を使用する場合や直接再コンポーズする場合よりも時間がかかることがあります。

ラムダのメモ化

強いスキップモードでは、コンポーザブル内のラムダのメモ化も強化されます。強いスキップを有効にすると、コンポーズ可能な関数内のすべてのラムダが自動的に記憶されます。

例

強力なスキップを使用するときにコンポーザブル内のラムダのメモ化を実現するため、コンパイラはラムダを remember 呼び出しでラップします。ラムダのキャプチャをキーとして使用します。

次の例のように、ラムダがある場合を考えてみましょう。

@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
    val lambda = {
        use(unstableObject)
        use(stableObject)
    }
}

強いスキップを有効にすると、コンパイラはラムダを remember 呼び出しでラップしてメモ化します。

@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
    val lambda = remember(unstableObject, stableObject) {
        {
            use(unstableObject)
            use(stableObject)
        }
    }
}

キーは、コンポーズ可能な関数と同じ比較ルールに従います。ランタイムは、インスタンスの等価性を使用して不安定なキーを比較します。オブジェクトの等価性を使用して、安定したキーを比較します。

メモ化と再コンポーズ

この最適化により、再コンポーズ中にランタイムがスキップするコンポーザブルの数が大幅に増加します。メモ化を行わないと、ランタイムは再コンポーズ中にラムダ パラメータを受け取るコンポーザブルに新しいラムダを割り当てる可能性が高くなります。その結果、新しいラムダには、最後の合成と等しくないパラメータが含まれます。これにより、再コンポジションが発生します。

メモ化を避ける

メモ化しないラムダがある場合は、@DontMemoize アノテーションを使用します。

val lambda = @DontMemoize {
    ...
}

APK サイズ

コンパイル時に、スキップ可能なコンポーザブルは、スキップ可能でないコンポーザブルよりも多くのコードを生成します。強いスキップを有効にすると、コンパイラはほぼすべてのコンポーザブルをスキップ可能としてマークし、すべてのラムダを remember{...} でラップします。そのため、強いスキップモードを有効にしても、アプリの APK サイズへの影響はごくわずかです。

Now in Android で強力なスキップを有効にすると、APK サイズが 4 KB 増加しました。サイズの差は、特定のアプリに存在する、以前はスキップできなかったコンポーズ可能な関数の数に大きく依存しますが、比較的軽微なはずです。