強いスキップは、Compose コンパイラで使用できるモードです。有効にすると、コンパイラの動作が次の 2 つの方法で変更されます。
強いスキップモードを有効にする
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 増加しました。サイズの差は、特定のアプリに存在する、以前はスキップできなかったコンポーズ可能な関数の数に大きく依存しますが、比較的軽微なはずです。