Compose の状態のライフスパン

Jetpack Compose では、コンポーズ可能な関数は多くの場合、remember 関数を使用して状態を保持します。State and Jetpack Compose で 説明されているように、保存された値は再コンポーズ全体で再利用できます。

remember は再コンポーズ全体で値を保持するツールとして機能しますが、状態はコンポジションの存続期間を超えて存続する必要があることがよくあります。このページでは、 rememberretainrememberSaveablerememberSerializable API の違い、どの API を選択すべきか、Compose で保存された値と保持された値を管理するための ベスト プラクティスについて説明します。

適切な存続期間を選択する

Compose には、コンポジション全体で状態を保持するために使用できる関数がいくつかあります。rememberretainrememberSaveable、および rememberSerializable です。これらの関数は存続期間とセマンティクスが異なり、それぞれ特定の種類の状態を保存するのに適しています。違いは次の表にまとめられています。

remember

retain

rememberSaveablerememberSerializable

値は再コンポーズ後も保持されますか?

値はアクティビティの再作成後も保持されますか?

常に同じ(===)インスタンスが返されます

同等の(==)オブジェクトが返されます(逆シリアル化されたコピーの場合もあります)

値はプロセスの終了後も保持されますか?

サポートされるデータタイプ

すべて

アクティビティが破棄された場合にリークするオブジェクトを参照しないでください

シリアル化可能である必要があります
(カスタム Saver または kotlinx.serialization を使用)

ユースケース

  • コンポジションにスコープ設定されたオブジェクト
  • コンポーズ可能な関数の構成オブジェクト
  • UI の忠実性を損なうことなく再作成できる状態
  • キャッシュ
  • 存続期間の長いオブジェクトまたは「マネージャー」オブジェクト
  • ユーザー入力
  • テキスト フィールドの入力、スクロール状態、切り替えなど、アプリで再作成できない状態

remember

remember は、Compose で状態を保存する最も一般的な方法です。remember が 初めて呼び出されると、指定された計算が実行され、 保存されます。つまり、コンポーズ可能な関数で後で再利用できるように Compose によって保存されます。コンポーズ可能な関数が再コンポーズされると、コードが再度実行されますが、remember の呼び出しでは、計算が再度実行されるのではなく、以前のコンポジションの値が返されます。

コンポーズ可能な関数の各インスタンスには、保存された値の独自のセットがあります。これは位置によるメモ化と呼ばれます。 保存された値が再コンポーズ全体で使用できるようにメモ化されると、コンポジション階層内の位置に関連付けられます。コンポーズ可能な関数が異なる場所で使用されている場合、コンポジション階層内の各インスタンスには、保存された値の独自のセットがあります。

保存された値が使用されなくなると、破棄され、レコードが削除されます。 保存された値は、コンポジション階層から削除された場合(key コンポーズ可能な関数または MovableContent を使用せずに別の場所に移動するために値が削除されて再度追加された場合を含む)、または異なる key パラメータで呼び出された場合に破棄されます。

使用可能な選択肢の中で、remember は存続期間が最も短く、このページで説明する 4 つのメモ化関数のうち最も早く値を破棄します。 このため、次の用途に最適です。

  • スクロール位置やアニメーションの状態などの内部状態オブジェクトの作成
  • 再コンポーズごとにコストのかかるオブジェクトの再作成を回避する

ただし、次のことは避ける必要があります。

  • remember を使用してユーザー入力を保存する。保存されたオブジェクトは、アクティビティの構成変更とシステムによって開始されたプロセスの終了をまたいで破棄されるため。

rememberSaveablerememberSerializable

rememberSaveablerememberSerializableremember をベースに構築されています。このガイドで説明するメモ化関数の中で最も存続期間が長いです。 再コンポーズ全体でオブジェクトを位置的にメモ化するだけでなく、値を保存して、構成変更やプロセスの終了(通常、フォアグラウンド アプリのメモリを解放するため、またはアプリの実行中にユーザーがアプリの権限を取り消した場合に、システムがバックグラウンドでアプリのプロセスを強制終了する場合)など、アクティビティの再作成全体で復元できるようにします。

rememberSerializablerememberSaveable と同様に機能しますが、kotlinx.serialization ライブラリでシリアル化可能な複雑な型の永続化を自動的にサポートします。型が @Serializable でマークされている(またはマークできる)場合は rememberSerializable を選択し、それ以外の場合は rememberSaveable を選択します。

このため、rememberSaveablerememberSerializable はどちらも、テキスト フィールドの入力、スクロール位置、切り替え状態など、ユーザー入力に関連付けられた状態を保存するのに最適です。ユーザーが位置を見失わないように、この状態を保存する必要があります。一般に、データベースなど、別の永続データソースからアプリが取得できない状態をメモ化するには、rememberSaveable または rememberSerializable を使用する必要があります。

rememberSaveablerememberSerializable は、メモ化された値を Bundle にシリアル化して保存します。これには次の 2 つの結果があります。

  • メモ化する値は、プリミティブ(IntLongFloatDouble を含む)、String、またはこれらの型の配列のいずれかで表す必要があります。
  • 保存された値が復元されると、以前にコンポジションで使用されたものと同じ参照(===)ではなく、同等の (==)新しいインスタンスになります。

kotlinx.serialization を使用せずに複雑なデータ型を保存するには、カスタム Saver を実装して、オブジェクトをサポートされているデータ型にシリアル化および逆シリアル化します。Compose は、StateListMapSet などの一般的なデータ型をすぐに理解し、自動的にサポートされている型に変換します。次に、Size クラスの Saver の例を示します。listSaver を使用して、Size のすべてのプロパティをリストにパッキングすることで実装されます。

data class Size(val x: Int, val y: Int) {
    object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver(
        save = { listOf(it.x, it.y) },
        restore = { Size(it[0], it[1]) }
    )
}

@Composable
fun rememberSize(x: Int, y: Int) {
    rememberSaveable(x, y, saver = Size.Saver) {
        Size(x, y)
    }
}

retain

retain API は、値をメモ化する期間に関して、rememberrememberSaveable/rememberSerializable の間に位置します。保持された値は、保存された値とは異なるライフサイクルを持つため、名前が異なります。

値が保持されると、位置的にメモ化され、アプリの存続期間に関連付けられた別の存続期間を持つ セカンダリ データ構造に 保存されます。保持された値は、シリアル化せずに構成変更をまたいで保持できますが、プロセスの終了をまたいで保持することはできません。コンポジション階層が再作成された後に値が使用されない場合、保持された値は破棄されます(これは、retain の破棄に相当します)。

rememberSaveable よりも短いライフサイクルと引き換えに、retain は、ラムダ式、フロー、ビットマップなどの大きなオブジェクトなど、シリアル化できない値を保持できます。たとえば、retain を使用してメディア プレーヤー(ExoPlayer など)を管理し、構成変更中にメディアの再生が中断されないようにすることができます。

@Composable
fun MediaPlayer() {
    // Use the application context to avoid a memory leak
    val applicationContext = LocalContext.current.applicationContext
    val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() }
    // ...
}

retainViewModel

どちらも、構成変更全体でオブジェクト インスタンスを保持する最も一般的な機能で、同様の機能を提供します。retainViewModelretain または ViewModel を選択するかどうかは、保持する値の型、スコープ設定の方法、追加機能が必要かどうかによって異なります。

ViewModelは、通常、アプリの UI レイヤとデータレイヤ間の通信をカプセル化するオブジェクトです。これにより、コンポーズ可能な関数からロジックを移動できるため、テストのしやすさが向上します。ViewModelViewModelStore 内でシングルトンとして管理され、保持された値とは存続期間が異なります。ViewModelViewModelStore が 破棄されるまでアクティブな状態を維持しますが、保持された値は、コンテンツがコンポジションから完全に削除されると破棄されます(たとえば、構成変更の場合、UI 階層が再作成され、コンポジションの再作成後に保持された値が使用されなかった場合、保持された値は破棄されます)。

ViewModel には、Dagger と Hilt を使用した依存関係インジェクション、SavedState との統合、バックグラウンド タスクを起動するための組み込みコルーチン サポートも含まれています。このため、ViewModel は、バックグラウンド タスクとネットワーク リクエストの起動、プロジェクト内の他のデータソースとのやり取り、必要に応じて、ViewModel の構成変更全体で保持され、プロセスの終了をまたいで保持されるミッション クリティカルな UI 状態のキャプチャと永続化に最適です。

retain は、特定のコンポーズ可能なインスタンスにスコープ設定され、兄弟コンポーズ可能な関数間での再利用や共有を必要としないオブジェクトに最適です。ViewModel は UI 状態を保存してバックグラウンド タスクを実行するのに適していますが、retain は、キャッシュ、インプレッション トラッキングと分析、AndroidView への依存関係、Android OS とやり取りするオブジェクトや、決済処理や広告などのサードパーティ ライブラリを管理するオブジェクトなど、UI パイプライン用のオブジェクトを保存するのに適しています。

最新の Android アプリ アーキテクチャの推奨事項以外のカスタムアプリ アーキテクチャ パターンを設計する上級ユーザー向け: retain を使用して、社内の「ViewModel-like」API を構築することもできます。コルーチンと保存された状態のサポートはすぐに利用できるわけではありませんが、retain は、これらの機能が組み込まれた ViewModel のライフサイクルの構成要素として機能します。このようなコンポーネントの設計方法の詳細については、このガイドの範囲外です。

retain

ViewModel

スコープ設定

共有値はありません。各値はコンポジション階層内の特定のポイントで保持され、関連付けられます。別の場所で同じ型を保持すると、常に新しいインスタンスに対して動作します。

ViewModel は 内のシングルトンですViewModelStore

破棄

コンポジション階層から完全に離れる場合

ViewModelStore がクリアまたは破棄された場合

追加機能

オブジェクトがコンポジション階層内にあるかどうかにかかわらず、コールバックを受信できます

組み込みの coroutineScope、 のサポートSavedStateHandle、Hilt を使用して注入可能

オーナー

RetainedValuesStore

ViewModelStore

ユースケース

  • 個々のコンポーズ可能な インスタンスにローカルな UI 固有の値を永続化する
  • RetainedEffect を使用したインプレッション トラッキング
  • カスタムの「ViewModel-like」アーキテクチャ コンポーネントを定義するための構成要素
  • コードの整理とテストの両方のために、UI レイヤとデータレイヤ間のやり取りを別の クラスに抽出する
  • FlowState オブジェクトに変換し、構成変更によって中断されない suspend 関数を呼び出す
  • 画面全体など、大きな UI 領域で状態を共有する
  • View との相互運用性

retainrememberSaveable または rememberSerializable を組み合わせる

オブジェクトに retainedrememberSaveable または rememberSerializable の両方のハイブリッドな存続期間が必要になることがあります。これは、オブジェクトが ViewModel の保存済み状態モジュール ガイドで説明されているように、保存された状態をサポートできる ViewModel である必要があることを示している可能性があります。

retainrememberSaveable または rememberSerializable を同時に使用できます。両方のライフサイクルを正しく組み合わせると、複雑さが大幅に増します。 このパターンは、より高度なカスタム アーキテクチャ パターンの一部として、次のすべてに該当する場合にのみ使用することをおすすめします。

  • 保持または保存する必要がある値の組み合わせで構成されるオブジェクトを定義している(ユーザー入力をトラッキングするオブジェクトや、ディスクに書き込めないインメモリ キャッシュなど)
  • 状態がコンポーズ可能な関数にスコープ設定されており、ViewModel のシングルトン スコープ設定や存続期間には適していない

これらすべてに該当する場合は、クラスを 3 つの部分に分割することをおすすめします。保存されたデータ、保持されたデータ、独自の状態を持たない「メディエーター」オブジェクトです。このオブジェクトは、保持されたオブジェクトと保存されたオブジェクトに委任して、状態を適切に更新します。このパターンは次のようになります。

@Composable
fun rememberAndRetain(): CombinedRememberRetained {
    val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) {
        ExtractedSaveData()
    }
    val retainData = retain { ExtractedRetainData() }
    return remember(saveData, retainData) {
        CombinedRememberRetained(saveData, retainData)
    }
}

@Serializable
data class ExtractedSaveData(
    // All values that should persist process death should be managed by this class.
    var savedData: AnotherSerializableType = defaultValue()
)

class ExtractedRetainData {
    // All values that should be retained should appear in this class.
    // It's possible to manage a CoroutineScope using RetainObserver.
    // See the full sample for details.
    var retainedData = Any()
}

class CombinedRememberRetained(
    private val saveData: ExtractedSaveData,
    private val retainData: ExtractedRetainData,
) {
    fun doAction() {
        // Manipulate the retained and saved state as needed.
    }
}

状態を存続期間で分離することで、責任とストレージの分離が非常に明確になります。保存されたデータは保持データによって操作できないように意図的に設計されています。これにより、savedInstanceState バンドルがすでにキャプチャされていて更新できない場合に、保存されたデータの更新が試行されるシナリオを防ぐことができます。また、Compose を呼び出したり、アクティビティの再作成をシミュレートしたりすることなく、コンストラクタをテストすることで、再作成シナリオをテストすることもできます。

このパターンの実装方法の完全な例については、完全なサンプル(RetainAndSaveSample.kt)をご覧ください。

位置によるメモ化とアダプティブ レイアウト

Android アプリケーションは、スマートフォン、折りたたみ式デバイス、タブレット、デスクトップなど、さまざまなフォーム ファクタをサポートできます。アプリケーションは、アダプティブ レイアウトを使用して、これらのフォーム ファクタ間を頻繁に移行する必要があります。たとえば、タブレットで実行されているアプリは、2 列のリスト詳細ビューを表示できますが、小さいスマートフォンの画面に表示される場合は、リストページと詳細ページの間を移動する場合があります。

保存された値と保持された値は位置的にメモ化されるため、コンポジション階層内の同じポイントに表示される場合にのみ再利用されます。レイアウトがさまざまなフォーム ファクタに適応すると、コンポジション階層の構造が変更され、値が破棄される可能性があります。

ListDetailPaneScaffoldNavDisplay(Jetpack Navigation 3)などのすぐに使えるコンポーネントの場合、これは問題ではなく、レイアウトの変更全体で状態が保持されます。フォーム ファクタに適応するカスタム コンポーネントの場合は、次のいずれかを行うことで、レイアウトの変更によって状態が影響を受けないようにします。

  • ステートフル コンポーズ可能な関数が、コンポジション階層内の同じ場所で常に呼び出されるようにします。コンポジション階層内のオブジェクトを再配置するのではなく、レイアウト ロジックを変更してアダプティブ レイアウトを実装します。
  • MovableContent を使用して、ステートフル コンポーズ可能な関数を適切に再配置します。MovableContent のインスタンスは、保存された値と保持された値を古い場所から新しい場所に移動できます。

ファクトリー関数を保存する

Compose UI はコンポーズ可能な関数で構成されていますが、コンポジションの作成と整理には多くのオブジェクトが使用されます。最も一般的な例は、 独自の状態を定義する複雑なコンポーズ可能なオブジェクトです。たとえば、LazyList これは LazyListState を受け入れます。

Compose に重点を置いたオブジェクトを定義する場合は、存続期間とキー入力の両方を含む、目的の保存動作を定義する remember 関数を作成することをおすすめします。これにより、状態のコンシューマーは、コンポジション階層内でインスタンスを確実に作成できます。このインスタンスは、想定どおりに存続し、無効になります。コンポーズ可能なファクトリー関数を定義する場合は、次のガイドラインに従ってください。

  • 関数名の先頭に remember を付けます。必要に応じて、関数の 実装がオブジェクトの retained に依存し、API が remember の別のバリエーションに依存するように進化しない場合は、代わりに retain 接頭辞 を使用します。
  • 状態の永続化が選択され、正しい Saver 実装を記述できる場合は、rememberSaveable または rememberSerializable を使用します。
  • 使用に関連しない可能性がある CompositionLocal に基づいて、副作用を回避するか、値を初期化します。状態が作成される場所が、使用される場所とは異なる場合があります。

@Composable
fun rememberImageState(
    imageUri: String,
    initialZoom: Float = 1f,
    initialPanX: Int = 0,
    initialPanY: Int = 0
): ImageState {
    return rememberSaveable(imageUri, saver = ImageState.Saver) {
        ImageState(
            imageUri, initialZoom, initialPanX, initialPanY
        )
    }
}

data class ImageState(
    val imageUri: String,
    val zoom: Float,
    val panX: Int,
    val panY: Int
) {
    object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver(
        save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) },
        restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) }
    )
}