このページでは、コンポーザブルのライフサイクルと、コンポーザブルに再コンポーズが必要かどうかを Compose が判断する仕組みについて説明します。
ライフサイクルの概要
状態の管理に関するドキュメントで説明されているように、Composition はアプリの UI の記述であり、コンポーザブルの実行により生成されます。Composition は、UI を記述するコンポーザブルのツリー構造です。
Jetpack Compose は、初回コンポジションで初めてコンポーザブルを実行する際に、Composition の UI を記述するために呼び出されるコンポーザブルをトラッキングします。その後、アプリの状態が変化すると、Jetpack Compose は再コンポジションをスケジュール設定します。再コンポジションの際、Jetpack Compose は状態の変化に応じて変化した可能性があるコンポーザブルを再実行し、変化を反映するために Composition を更新します。
Composition は、初回コンポジションによってのみ作成され、再コンポジションによってのみ更新されます。Composition を変更する唯一の方法は、再コンポジションを行うことです。
図 1. Composition におけるコンポーザブルのライフサイクル。コンポーザブルは Composition に入場し、0 回以上再コンポースされ、Composition から退場します。
通常、再コンポジションは State<T>
オブジェクトの変更によってトリガーされます。Compose はそうした変更をトラッキングし、特定の State<T>
を読み取る Composition 内のすべてのコンポーザブルと、スキップできない呼び出し対象コンポーザブルを実行します。
コンポーザブルが複数回呼び出されると、複数のインスタンスが Composition 内に配置されます。各呼び出しには、Composition における固有のライフサイクルがあります。
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
図 2. Composition 内の MyComposable
を表した図。コンポーザブルが複数回呼び出されると、複数のインスタンスが Composition 内に配置されます。色が異なる要素は、別のインスタンスであることを示します。
Composition 内のコンポーザブルの構造
Composition 内のコンポーザブルのインスタンスは、そのコールサイトによって識別されます。Compose コンパイラは、各コールサイトを別個のものと見なします。複数のコールサイトからコンポーザブルを呼び出すと、コンポーザブルの複数のインスタンスが Composition 内に作成されます。
再コンポジションの際にコンポーザブルが前回のコンポジションのときと異なるコンポーザブルを呼び出した場合、Compose はどのコンポーザブルが呼び出され、どのコンポーザブルが呼び出されなかったかを識別し、両方のコンポジションで呼び出されたコンポーザブルについては、入力が変化していなければ再コンポジションを回避します。
ID の保持は、副作用をコンポーザブルに関連付けるために必要不可欠です。これにより、コンポーザブルは再コンポジションのたびに再起動されることなく、正常に完了することができます。
次の例を考えてみましょう。
@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ }
上記のコード スニペットでは、LoginScreen
は条件付きで LoginError
コンポーザブルを呼び出し、常に LoginInput
コンポーザブルを呼び出します。各呼び出しには一意のコールサイトとソース位置があり、コンパイラはそれらを使用して呼び出しを一意に識別します。
図 3. 状態が変化して再コンポジションが発生したときの Composition 内の LoginScreen
を表した図。同じ色の要素は、再コンポーズされていないことを示します。
LoginInput
が初めて呼び出されてから二度目に呼び出された場合でも、LoginInput
インスタンスは再コンポジションの前後で保持されます。さらに、LoginInput
には再コンポジションの前後で変化したパラメータがないため、LoginInput
の呼び出しは Compose によってスキップされます。
スマートな再コンポジションに役立つ情報を追加する
コンポーザブルを複数回呼び出すと、複数回 Composition に追加されます。あるコンポーザブルを同じコールサイトから複数回呼び出した場合、Compose はそのコンポーザブルの各呼び出しを一意に識別する情報を持っていないため、インスタンスを区別する情報として、コールサイトに加えて実行順序が使用されます。この動作は十分に必要を満たすこともありますが、場合によっては望ましくない動作を引き起こす可能性があります。
@Composable fun MoviesScreen(movies: List<Movie>) { Column { for (movie in movies) { // MovieOverview composables are placed in Composition given its // index position in the for loop MovieOverview(movie) } } }
上記の例では、Compose はコールサイトに加えて実行順序を使用し、Composition 内のインスタンスを区別しています。新しい movie
がリストの末尾に追加された場合、Compose は、すでに Composition 内にあるインスタンスを再利用できます。リスト内の位置が変化せず、したがってそれらのインスタンスでは movie
入力が同一であるからです。
図 4. 新しい要素がリストの末尾に追加されたときの Composition 内の MoviesScreen
を表した図。Composition 内の MovieOverview
コンポーザブルは再利用可能です。同じ色の MovieOverview
は、コンポーザブルが再コンポーズされなかったことを示します。
一方、movies
リストの先頭または途中にアイテムが追加されるか、アイテムが削除されたり並べ替えられたりしてリストが変更された場合は、入力パラメータのリスト内の位置が変更されたすべての MovieOverview
呼び出しで再コンポジションが発生します。たとえば、MovieOverview
が副作用を使用して映画画像を取得する場合、この点は非常に重要です。作用の進行中に再コンポジションが発生すると、作用はキャンセルされ、再起動されます。
@Composable fun MovieOverview(movie: Movie) { Column { // Side effect explained later in the docs. If MovieOverview // recomposes, while fetching the image is in progress, // it is cancelled and restarted. val image = loadNetworkImage(movie.url) MovieHeader(image) /* ... */ } }
図 5. 新しい要素がリストに追加されたときの Composition 内の MoviesScreen
を表した図。MovieOverview
コンポーザブルは再利用できず、すべての副作用が再起動されます。色が異なる MovieOverview
は、コンポーザブルが再コンポーズされたことを示します。
理想としては、MovieOverview
インスタンスの ID はインスタンスに渡された movie
の ID にリンクされていると考えることができます。映画のリストを並べ替える場合は、Composition ツリー内のインスタンスを同様の方法で並べ替えるのが理想的です。そうすれば、異なる映画インスタンスで個々の MovieOverview
コンポーザブルを再コンポーズせずに済みます。Compose は、ツリーの特定の部分(key
コンポーザブル)を識別するために使用したい値をランタイムに通知する方法を備えています。
キー コンポーザブルを呼び出すコードブロックを、渡された 1 つ以上の値でラップすることにより、それらの値が結合され、Composition 内の該当インスタンスを識別するために使用されます。key
の値は、グローバルに一意である必要はありません。コールサイトのコンポーザブルの呼び出しの中で一意であれば十分です。したがって、この例では、各 movie
は movies
の中で一意の key
を持つ必要があります。その key
をアプリの他の場所にある他のコンポーザブルと共有してもかまいません。
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
上記の例では、リストの要素が変更された場合でも、Compose は MovieOverview
の個々の呼び出しを認識してそれらを再利用できます。
図 6. 新しい要素がリストに追加されたときの Composition 内の MoviesScreen
を表した図。MovieOverview
コンポーザブルは一意のキーを持っているため、Compose は変化していない MovieOverview
インスタンスを認識して、それらを再利用できます。それらの副作用は引き続き実行されます。
一部のコンポーザブルは、key
コンポーザブルの組み込みサポートを備えています。たとえば、LazyColumn
は、items
DSL 内のカスタム key
の指定を受け入れます。
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
入力が変化していない場合にスキップする
再コンポジション中に、対象となるコンポーザブル関数の入力が前回のコンポジションから変更されていない場合、その関数の実行が完全にスキップされることがあります。
コンポーズ可能な関数は、次の条件を満たさない限りスキップできます。
- 関数の戻り値の型が
Unit
以外である - 関数に
@NonRestartableComposable
または@NonSkippableComposable
アノテーションが付けられている - 必須パラメータの型が安定していない
試験運用版のコンパイラ モードであるStrong Skipping では、最後の要件が緩和されます。
型が安定していると見なされるには、次のコントラクトに準拠している必要があります。
- 2 つのインスタンスの
equals
の結果が、同じ 2 つのインスタンスについて常に同じになる。 - 型の公開プロパティが変化すると、Composition に通知される。
- すべての公開プロパティの型も安定している。
このコントラクトに従う型の中には、明示的に @Stable
アノテーションによって安定しているとマークされていなくても、Compose コンパイラが安定しているものとして扱う重要で一般的な型がいくつかあります。
- すべてのプリミティブ値型:
Boolean
、Int
、Long
、Float
、Char
など - 文字列
- すべての関数型(ラムダ)
これらの型はすべて不変であるため、安定性のコントラクトに従うことが可能です。不変の型は決して変化しないので、Composition に変化を通知する必要がありません。したがって、このコントラクトに従う方がはるかに簡単です。
安定しているが可変である型の代表例は、Compose の MutableState
型です。値が MutableState
に保持されている場合、State
の .value
プロパティに対する変更は Compose に通知されるため、状態オブジェクトは全体として安定していると見なされます。
コンポーザブルにパラメータとして渡されるすべての型が安定している場合、UI ツリー内のコンポーザブルの位置に基づいてパラメータ値が等しいかどうかが比較されます。前回の呼び出し以降、すべての値が変化していなければ、再コンポジションはスキップされます。
Compose は、証明できる場合にのみ、型を安定していると見なします。たとえば、インターフェースは一般的に安定していないものとして扱われます。実装が不変である可能性がある可変の公開プロパティを持つ型も安定していません。
Compose が安定していると推測できない型を安定しているものとして扱うことを Compose に強制するには、@Stable
アノテーションでマークします。
// Marking the type as stable to favor skipping and smart recompositions. @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasError: Boolean get() = exception != null }
上記のコード スニペットでは、UiState
はインターフェースであるため、Compose は、通常はこの型を安定していないと見なす可能性があります。@Stable
アノテーションを追加すると、この型が安定していると Compose に伝えて、Compose にスマートな再コンポジションを選択させることができます。また、インターフェースがパラメータ型として使用されている場合、Compose はそのすべての実装を安定しているものとして扱います。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- 状態と Jetpack Compose
- Compose における副作用
- Compose で UI 状態を保存する