UI の状態の保存

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

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

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

このページでは、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 オブジェクトは onSaveInstanceState()(または他の永続ディスク ストレージ)と組み合わせて使用し、識別子を savedInstanceState に保管して、システムの終了後にビューモデルがデータを再読み込みできるようにする必要があります。

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

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

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

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

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

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

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

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

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

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 コントローラを表示するのに必要なすべてのデータがメモリに保存されます。
    • 例: 最近の検索結果の曲オブジェクト、最近の検索クエリ。
  • onSaveInstanceState(): UI コントローラが停止された後に再作成されるときに、アクティビティの状態を簡単に再読み込みするのに必要な少量のデータが保存されます。複雑なオブジェクトは、ここではなくローカル ストレージに保存します。onSaveInstanceState() には各オブジェクトの一意の ID が保存されます。
    • 例: 最近の検索クエリの保存。

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

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

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

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

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

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

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

参考情報

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

ブログ