UI の状態を保存する

このガイドでは、UI の状態に関するユーザーの期待と、状態を保持するために利用できるオプションについて説明します。

システムによってホスト アクティビティまたはアプリのプロセスが破棄された後、UI の状態をすばやく保存して復元することは、優れたユーザー エクスペリエンスに不可欠です。ユーザーは UI の状態が変わらないことを期待しますが、画面をホストする Activity とその保存されている状態はシステムによって破棄される可能性があります。

ユーザーの期待とシステムの動作のギャップを埋めるには、次の方法を組み合わせて使用します。

最適なソリューションは、UI データの複雑さ、アプリのユースケース、データアクセス速度とメモリ使用量のバランスによって決まります。

アプリがユーザーの期待を満たし、高速でレスポンシブなインターフェースを提供するようにします。UI にデータを読み込む際、特に回転などの一般的な構成変更の後で、遅延が発生しないようにします。

ユーザーの期待とシステムの動作

ユーザーは自分が行う操作に応じて、UI の状態がクリアされる、または保持されることを期待します。場合によっては、システムがユーザーの期待どおりに自動的に処理します。場合によっては、システムが逆の処理を行います。

ユーザーが開始する UI の状態の破棄

ユーザーは、画面に移動したとき、その画面を完全に終了するまでその時点での一時的な UI の状態が変わらないことを期待します。ユーザーは、次の操作を行うことで、画面またはアプリを完全に終了できます。

  • アプリをスワイプして概要(履歴)画面から削除する。
  • 設定画面でアプリを中止または強制終了する。
  • デバイスを再起動する。
  • なんらかの「終了」アクション(Activity.finish() によりサポート)を完了する。

これらの完全な終了ケースにおいては、ユーザーは自分が画面から完全に離れたと考えています。そのため、ユーザーは、画面をもう一度開いたときに、クリーンな状態から開始されることを期待します。これらの終了シナリオの基本的なシステム動作は、ユーザーの期待に一致します。つまり、ホスト アクティビティ インスタンスは、そのインスタンスに保存されている状態と、それに関連付けられた保存済み状態レコードとともに破棄され、メモリから削除されます。

完全な終了に関するこのルールには例外がいくつかあります。たとえば、戻るボタンを使用することで、ブラウザを終了する前に閲覧していたウェブページが再びブラウザに表示されることをユーザーが期待している場合があります。

システムによって開始される UI の状態の解除

ユーザーが期待するのは、構成の変更後も、たとえば回転やマルチウィンドウ モードへの切り替えが行われても画面の UI の状態が変わらないことです。 しかし、デフォルトでは、アクティビティは設定が変更されると破棄され、アクティビティ インスタンスに保存されている UI のすべての状態がワイプされます。デバイスの構成について詳しくは、 Jetpack Compose で構成の変更に対応するをご覧ください。

なお、構成変更のデフォルトの動作をオーバーライドすることもできます(ただし、推奨されません)。詳しくは、構成の変更を 処理するをご覧ください。

ユーザーはまた、一時的に別のアプリに切り替えた後に元のアプリに戻ってきたときも、アプリの UI の状態が変わっていないことを期待します。たとえば、ユーザーが画面で検索を実行した後、ホームボタンを押すか電話に出たユーザーは、その後検索画面に戻ったときに前とまったく同じ検索広告キーワードと検索結果がまだ表示されていることを期待します。

このシナリオではアプリはバックグラウンドにあり、システムは、アプリのプロセスをメモリに保持しようと最善を尽くします。しかし、ユーザーが元のアプリを離れ、他のアプリを操作している間にアプリのプロセスがシステムによって破棄されることがあります。この場合、ホスト アクティビティは、そのアクティビティに保存されているすべての状態と一緒に破棄されます。ユーザーがアプリを再起動すると、画面は期待に反してクリーンな状態になります。 プロセスの終了について詳しくは、プロセスとアプリのライフサイクルをご覧ください。

UI の状態の保持オプション

UI の状態に関するユーザーの期待がデフォルトのシステム動作と一致しない場合は、システムによる破棄がユーザーに影響を与えないよう、ユーザーの UI の状態を保存し、復元する必要があります。

UI の状態の保持オプションは、ユーザー エクスペリエンスに影響を与える次の項目によって異なります。

ViewModel 状態の保持 永続ストレージ
保存先 メモリ内 メモリ内 ディスクまたはネットワーク上
構成変更後も保持
システムによるプロセス終了後も保持 × はい はい
ユーザーが画面を完全に終了/finish() した後も保持 × いいえ
データの上限 複雑なオブジェクトは問題ないが、使用可能なメモリによってスペースが制限される プリミティブ型と String などのシンプルな小さなオブジェクトのみ ネットワーク リソースからのディスク スペースまたはネットワークからの取得コスト / 時間によってのみ制限される
読み取り / 書き込み時間 高速(メモリアクセスのみ) 低速(シリアル化 / シリアル化解除が必要) 低速(ディスク アクセスまたはネットワーク トランザクションが必要)

ViewModel を使用して構成の変更を処理する

ユーザーがアプリをアクティブに使用している場合は、ViewModel を使用して、UI 関連データを保存し管理することをおすすめします。これにより、UI データに迅速にアクセスでき、回転やウィンドウのサイズ変更などの一般的な構成変更の後もネットワークやディスクからのデータの再取得を回避できます。ViewModel の実装方法については、ViewModel ガイドをご覧ください。

ViewModel ではデータがメモリに保持されます。つまり、ディスクやネットワークからデータを取得するよりもコストがかかりません。ViewModel は、Navigation デスティネーションやアクティビティなどのライフサイクル所有者に関連付けられます。構成の変更中はメモリに保持され、構成の変更によって発生した新しいライフサイクル所有者インスタンスに、システムによって自動的に関連付けられます。

保存済み状態とは異なり、ViewModel はシステムによって開始されたプロセスの終了時に破棄されます。システムによって開始されたプロセスの終了後、ViewModel でデータの再読み込みを行うには、 SavedStateHandle APIを使用します。または、データが UI に関連していて ViewModel で保持する必要がない場合は、rememberSerializable を使用します。プリミティブ データ型の場合や、@Serializable を使用しない場合は、rememberSaveable を使用します。データがアプリデータである場合は、ディスクに保存する方が 適切です。

構成が変わっても UI の状態が保存されるメモリ内ソリューションがすでに存在する場合は、ViewModel を使用する必要はありません。

システムによって開始されたプロセスの終了を処理するために保存済み状態をバックアップとして使用する

Compose の rememberSerializablerememberSaveable、ViewModel の SavedStateHandle などの API は、システムがコンポーネントを破棄して再作成する場合に、UI の状態を再読み込みするために必要なデータを保存します。複雑なデータ構造をより効率的に処理するために、SavedStateHandlesaved {} 拡張機能を介して Kotlinx シリアル化をサポートしています。これにより、標準のプリミティブ型とともに型安全なオブジェクトをシームレスに保持して復元できます。rememberSaveable を使用して保存済み状態を実装する方法については、状態と Jetpack Compose をご覧ください。

保存済み状態のバンドルは、構成の変更後もプロセスの終了後も保持されますが、さまざまな API がデータのシリアル化を行うため、ストレージと速度が制限されます。シリアル化されるオブジェクトが複雑な場合、シリアル化によって大量のメモリが消費されることがあります。このプロセスは構成の変更時にメインスレッドで実行されるため、シリアル化に時間がかかりすぎるとフレームが欠けたり、画面がスムーズに表示されないことがあります。

大量のデータ(ビットマップや、シリアル化またはシリアル化解除に時間がかかる複雑なデータ構造など)を保存する場合は、保存済み状態を使用しないでください。これには、プリミティブ型と String などのシンプルな小さなオブジェクトのみを保存します。このように、ID などの必要な最小限のデータを保存する場合に保存済み状態を使用し、他の永続メカニズムが失敗した場合に UI を以前の状態に復元するために必要なデータを再作成します。ほとんどのアプリは、システムによって開始されたプロセスの終了を処理できるように、この方法を実装する必要があります。

アプリのユースケースによっては、保存済み状態を使用する必要がまったくない場合もあります。たとえば、ユーザーがブラウザを終了する前に閲覧していたウェブページがブラウザに再び表示されることがあります。アクティビティがこのように動作する場合は、保存済み状態を使用する代わりに、すべてをローカルで保持できます。

また、インテントからアクティビティを開くと、構成の変更時、およびシステムがアクティビティを復元するときに、エクストラのバンドルがアクティビティに配信されます。アクティビティが開始されたときに、検索クエリなどの UI 状態データがインテント エクストラとして渡された場合は、保存済み状態のバンドルの代わりにエクストラのバンドルを使用できます。インテント エクストラについて詳しくは、インテントとインテント フィルタをご覧ください。

これらのシナリオのいずれにおいても、ViewModelを使用して、 構成の変更時にデータベースからデータを再び読み込んでサイクルを無駄にしないようにしてください。

保持する UI データが単純かつ軽量である場合は、保存済み状態 API のみを使用して状態データを保持できます。

rememberSaveable

SavedStateRegistry を使用して保存済み状態に接続する

Fragment 1.1.0 およびその推移的な依存関係 Activity 1.0.0 以降、ComponentActivity などの UI コンポーネントは、 SavedStateRegistryOwner を実装し、そのコンポーネントにバインドされた SavedStateRegistry を提供するようになりました。SavedStateRegistry を使用すると、コンポーネントを保存済み状態に接続できます。これにより、保存済み状態を使用したり、コンポーネントを保存済み状態に寄与させることが可能になります。たとえば、 ViewModel の 保存済み状態モジュールは、SavedStateRegistry を使用して SavedStateHandle を作成し、ViewModel オブジェクトに渡します。SavedStateRegistry は、savedStateRegistry を呼び出すことでライフサイクル所有者内から取得できます。

保存済み状態に寄与するコンポーネントは、 SavedStateRegistry.SavedStateProviderを実装する必要があります。これは、 という単一のメソッドsaveState()を定義します。saveState() メソッドを使用すると、コンポーネントは、そのコンポーネントから保存する必要がある状態をすべて含む Bundle を返すことができます。SavedStateRegistry は、ライフサイクル所有者のライフサイクルの保存状態フェーズでこのメソッドを呼び出します。

  class SearchManager : SavedStateRegistry.SavedStateProvider {
      companion object {
          private const val QUERY = "query"
      }

      private val query: String? = null

      ...

      override fun saveState(): Bundle {
          return bundleOf(QUERY to query)
      }
  }

SavedStateProvider を登録するには、registerSavedStateProvider()SavedStateRegistry を呼び出して、プロバイダのデータと プロバイダに関連付けるキーを渡します。プロバイダの以前に保存されたデータは、 保存済み状態からconsumeRestoredStateForKey()SavedStateRegistryで呼び出し、プロバイダのデータに関連付けられたキーを渡すことにより取得できます。

ComponentActivity 内で、super.onCreate() を呼び出した後、 onCreate()SavedStateProvider を登録できます。または、SavedStateRegistryOwnerLifecycleObserver を設定し、 LifecycleOwner を実装するSavedStateProviderON_CREATE イベントが発生したら登録することもできます。LifecycleObserver を使用すると、以前に保存した状態の登録と取得を SavedStateRegistryOwner 自体から分離できます。

  class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
      companion object {
          private const val PROVIDER = "search_manager"
          private const val QUERY = "query"
      }

      private val query: String? = null

      init {
          // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
          registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
              if (event == Lifecycle.Event.ON_CREATE) {
                  val registry = registryOwner.savedStateRegistry

                  // Register this object for future calls to saveState()
                  registry.registerSavedStateProvider(PROVIDER, this)

                  // Get the previously saved state and restore it
                  val state = registry.consumeRestoredStateForKey(PROVIDER)

                  // Apply the previously saved state
                  query = state?.getString(QUERY)
              }
          }
      }

      override fun saveState(): Bundle {
          return bundleOf(QUERY to query)
      }

      ...
  }

  class SearchActivity : ComponentActivity() {
    private var searchManager = SearchManager(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set up your Compose UI here
        setContent {
            // ...
        }
    }
  }

永続ローカル ストレージを使用して、複雑なデータまたは大規模なデータのプロセスの終了を処理する

データベースや DataStore などの永続ローカル ストレージは、アプリがユーザーのデバイスにインストールされている間は(ユーザーがアプリのデータを削除しない限り)存続します。このようなローカル ストレージは、システムによって開始されたアプリのプロセスの終了後も存続しますが、ローカル ストレージからメモリに読み込む必要があるため、取得に時間がかかることがあります。一般的には、この永続ローカル ストレージは、アプリの開始時と終了時に失いたくないすべてのデータが保存されるアプリ アーキテクチャの一部になっている可能性があります。

ViewModel も rememberSerializablerememberSaveableSavedStateHandle を使用して保存された状態も、長期ストレージ ソリューションではないため、データベースなどのローカル ストレージの代わりにはなりません。代わりに、これらのメカニズムは一時的な UI の状態のみを一時的に保存するときに使用し、他のアプリデータには永続ストレージを使用する必要があります。ローカル ストレージを利用して、アプリモデルのデータを長期的に(たとえば、デバイスの再起動後も)保持する方法について詳しくは、アプリ アーキテクチャ ガイド をご覧ください。

UI の状態の管理: 分割統治

UI の状態を効率的に保存および復元するには、さまざまなタイプの永続メカニズム間で作業を分割します。ほとんどの場合、これらのメカニズムではそれぞれ異なるタイプのデータが、データの複雑さ、アクセス速度、全期間のトレードオフに基づいてアプリで使用され保存されます。

  • 永続ローカル ストレージ: アプリの開始時と終了時に失いたくないすべてのアプリデータを保存します。
    • 例: 曲オブジェクトのコレクション。これには、音声ファイルやメタデータが含まれる可能性があります。
  • ViewModel: 関連付けられている UI(画面の UI 状態)の表示に必要なすべてのデータをメモリ内に保存します。
    • 例: 最近の検索結果の曲オブジェクト、最近の検索クエリ。
  • 保存済み状態(rememberSerializablerememberSaveableSavedStateHandle): システムが停止して UI を再作成する場合に、UI の状態を再読み込みするために必要な少量のデータを保存します。複雑なオブジェクトはここではなくローカル ストレージに保存し、それらのオブジェクトの一意の ID は保存済み状態 API に保存します。
    • 例: 最近の検索クエリの保存。

例として、曲のライブラリを検索できるアプリを考えてみましょう。各種のイベントは次のように処理されます。

ユーザーが曲を追加すると、ViewModel によって即座にデリゲートされ、そのデータがローカルに保存されます。この新たに追加した曲を UI に表示する必要がある場合は、ViewModel オブジェクト内のデータを更新して曲の追加を反映する必要もあります。データベースへの挿入は、すべてメインスレッド以外で行ってください。

ユーザーが曲を検索したとき、データベースから読み込むすべての複雑な曲データは、画面 UI 状態の一部として直ちに ViewModel オブジェクトに保存する必要があります。

アプリがバックグラウンドに移動され、システムが状態を保存するときは、プロセスが再作成を行う場合に備えて、保存済み状態 API を使用して検索クエリを保存する必要があります。この場所に保存されているアプリデータを読み込むには情報が必要であるため、検索クエリを ViewModel の SavedStateHandle に保存するか、コンポーザブルで rememberSerializable または rememberSaveable を使用します。データを読み込んで UI を現在の状態に戻すために必要な情報は、これがすべてです。

複雑な状態の復元: 断片を組み立て直す

ユーザーがアプリに戻るとき、UI を再作成するには次の 2 つのシナリオが考えられます。

  • システムがアプリのプロセスを終了した後に UI が再作成されるケース。システムは、保存済み状態 API を使用してクエリを保存しています。ViewModelSavedStateHandle を使用)またはコンポーザブル(rememberSerializable または rememberSaveable を使用)がクエリを自動的に復元します。コンポーザブルがクエリを復元する場合は、クエリを ViewModel に渡します。ViewModel は、検索結果がキャッシュに保存されていないことを確認し、渡された検索クエリを使用して検索結果の読み込みをデリゲートします。
  • 構成の変更後に UI が再作成されるケース。ViewModel インスタンスは破棄されていないため、ViewModel はすべての情報をメモリ内でキャッシュに保存しており、データベースへのクエリを再実行する必要がありません。

参考情報

UI の状態の保存について詳しくは、以下のリソースをご覧ください。

Codelab

Views コンテンツ