UI の状態の保存

システムによって開始されたアクティビティ間や、アプリが破棄された場合に、アクティビティの UI の状態が適宜保持、復元されることは、ユーザー エクスペリエンスにとっては非常に重要です。この場合、ユーザーは UI の状態が変わらないことを期待しますが、アクティビティと、そのアクティビティに保存されている状態はすべて破棄されます。

ユーザーの期待とシステムの動作のギャップを埋めるには、ViewModel オブジェクト、onSaveInstanceState() メソッド、ローカル ストレージを組み合わせて使用し、このようなアプリとアクティビティのインスタンスが移行されている期間を通して UI の状態を保持します。これらのオプションをどのように組み合わせるかは、UI データの複雑さ、アプリのユースケースによって決まります。また、取得速度とメモリ使用量を考慮する必要もあります。

どのアプローチを採用する場合でも、アプリの UI の状態はユーザーの期待を満たす必要があります。また、UI はスムーズかつ快適に操作できなければなりません。特に頻繁に発生する回転などの設定変更の後は、UI へのデータ読み込みで遅延が発生しないようにします。ほとんどの場合、ViewModel と onSaveInstanceState の両方を使用する必要があります。

このページでは、UI の状態に対するユーザーの期待、状態を保持するためのオプション、それぞれのトレードオフおよび制限について説明します。

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

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

ユーザーが開始する UI の状態の解除

アクティビティを開始したユーザーは、そのアクティビティを完全に終了するまでその時点での一時的な UI の状態が変わらないことを期待します。ユーザーは、次の方法でアクティビティを完全に終了できます。

  • 戻るボタンを押す
  • アクティビティをスワイプして概要(最近の)画面から離れる
  • アクティビティから上へ移動する
  • 設定画面からアプリを強制的に終了する
  • ある種の「終了」アクティビティを完了する(Activity.finish() がサポート)

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

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

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

ユーザーが期待するのは、設定が変更されても、たとえば回転やマルチウィンドウ モードへの切り替えが行われてもアクティビティの UI の状態が変わらないことです。しかし、デフォルトでは、アクティビティは設定が変更されると破棄され、アクティビティ インスタンスに保存されている UI のすべての状態がワイプされます。デバイスの設定について詳しくは、設定のリファレンス ページをご覧ください。設定変更のデフォルトの動作は、オーバーライドできることに注意してください(ただし、推奨されません)。詳しくは、設定の変更を自分で処理するをご覧ください。

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

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

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

UI の状態に関するユーザーの期待がデフォルトのシステム動作と一致しない場合は、システムによって開始された破棄が確実にユーザーにわかるように、ユーザーの UI の状態を保存し、復元する必要があります。

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

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

ViewModel を使用して設定の変更を処理する

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

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

ViewModel は、ユーザーがアクティビティやフラグメントを取り消したとき、または finish() を呼び出したときに自動的に破棄されます。つまり、こうしたシナリオでは、ユーザーの期待どおりに状態がクリアされます。

保存済みインスタンスの状態とは異なり、ViewModel はシステムによって開始されたプロセスの終了時に破棄されます。そのため、ViewModel オブジェクトは、onSaveInstanceState()(または他の永続ディスク ストレージ)と組み合わせて使用し、識別子を savedInstanceState に保管して、システムの終了後にビューモデルがデータを再読み込みできるようする必要があります。

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

onSaveInstanceState() をバックアップとして使用して、システムによって開始されたプロセスの終了を処理する

onSaveInstanceState() コールバックには、システムがアクティビティやフラグメントなどの UI コントローラを破棄して後で再作成する場合に、その UI コントローラの状態を再読み込みするのに必要なデータが保存されます。保存済みインスタンスの状態を実装する方法については、アクティビティ ライフサイクル ガイドのアクティビティの状態の保存と復元に関するトピックをご覧ください。

保存済みインスタンス状態のバンドルは、設定の変更とプロセス終了の両方を保持しますが、onSavedInstanceState() によってデータがディスクにシリアル化されるため、保存容量と速度によって制限されます。シリアル化されるオブジェクトが複雑な場合、シリアル化によって大量のメモリが消費されることがあります。このプロセスは構成の変更時にメインスレッドで実行されるため、シリアル化に時間がかかりすぎるとフレームが欠けたり、映像が途切れたりすることがあります。

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

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

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

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

保持する UI データが単純で軽量な場合、onSaveInstanceState() のみを使用して状態データを保持できます。

注: ViewModel の保存済み状態モジュールViewModel オブジェクトの保存済み状態へのアクセスを提供できるようになりました。この保存済み状態には、SavedStateHandle と呼ばれるオブジェクトを介してアクセスできます。使用方法については、Android ライフサイクル対応コンポーネントのコードラボをご覧ください。

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

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

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

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

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

  • 永続ローカル ストレージ: アクティビティの開始および終了時に失いたくないデータすべてが保存されます。
    • 例: 曲オブジェクトのコレクション。これには、音声ファイルやメタデータが含まれる可能性があります。
  • ViewModel: 関連する UI コントローラを表示するのに必要なすべてのデータがメモリに保存されます。
    • 例: 最近の検索結果の曲オブジェクト、最近の検索クエリ。
  • onSaveInstanceState(): UI コントローラが停止された後に再作成されるときに、アクティビティの状態を簡単に再読み込みするのに必要な少量のデータが保存されます。複雑なオブジェクトは、ここではなくローカル ストレージに保存します。onSaveInstanceState() には各オブジェクトの一意の ID が保存されます。
    • 例: 最近の検索クエリの保存。

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

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

ユーザーが曲を検索するときには、UI コントローラ用のデータベースから読み込む複雑な曲データはすべて ViewModel オブジェクトに即座に保存する必要があります。また、検索クエリ自体を ViewModel オブジェクトに保存する必要もあります。

アクティビティがバックグラウンドに移動されると、onSaveInstanceState() が呼び出されます。検索クエリは onSaveInstanceState() バンドルに保存する必要があります。この少量のデータは簡単に保存できます。アクティビティを現在の状態に戻すのに必要な情報はこれだけです。

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

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

  • アクティビティがシステムによって停止された後に再作成されるケース。アクティビティがクエリを onSaveInstanceState() バンドルに保存して、そのクエリを ViewModel に渡す必要があります。ViewModel は、検索結果がキャッシュされていないことを確認し、渡された検索クエリを使用して検索結果の読み込みを委任します。
  • 構成の変更後にアクティビティが作成されるケース。アクティビティがクエリを onSaveInstanceState() バンドルに保存します。検索結果は、ViewModel によってすでにキャッシュされています。クエリを onSaveInstanceState() バンドルから ViewModel に渡すと、必要なデータがすでに読み込まれていること、データベースに対してクエリを再実行する必要がないことが確定します。

注: アクティビティが最初に作成されたとき、onSaveInstanceState() バンドルにデータは含まれておらず、ViewModel オブジェクトは空です。ViewModel オブジェクトを作成したら、空のクエリを渡します。これにより、読み込むデータがまだないことを ViewModel オブジェクトに伝わります。したがって、アクティビティは空の状態で開始されます。

その他のリソース

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

ブログ