UI の状態を保存する

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Kotlin

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)
    }
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

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

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

Kotlin

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 SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

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

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

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

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

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

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

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

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

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

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

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

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

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

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

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

  • アクティビティがシステムによって停止された後に再作成されるケース。システムはクエリを保存済みインスタンス状態のバンドルに保存しています。SavedStateHandle が使用されていない場合、UI はクエリを ViewModel に渡す必要があります。ViewModel は、検索結果がキャッシュに保存されていないことを確認し、渡された検索クエリを使用して検索結果の読み込みをデリゲートします。
  • 構成の変更後にアクティビティが作成されるケース。ViewModel インスタンスは破棄されていないため、ViewModel はすべての情報をメモリ内でキャッシュに保存しており、データベースへのクエリを再実行する必要がありません。

参考情報

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

ブログ