【Kotlin/Android】SavedStateHandleの使い方!ViewModelの状態管理と復元
この記事からわかること
- Kotlin/AndroidでSavedStateHandleの使い方
- 状態保存の仕組み
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Narwhal Feature Drop
- Android OS:15以降
- Kotlin:2.2.21
- AGP:8.12.3
- Gradle:8.13
- macOS(M1):Tahoe 26.0.1
AndroidアプリのUI状態の保存
Androidは画面の回転など特定の動作などによりシステムによってActivityやアプリが破棄されデータが消失してしまうことが多く発生します。そのためUIの状態を一時的に保存して復元するような仕組みを構築しておくことがユーザーエクスペリエンスを維持する上で重要になってきます。
UIの状態を一時的に保存するための仕組みは以下のようにいくつか用意されていますがユースケースに応じて最適なものを取捨選択する必要があります。
※ Saved instance stateは基本的にユーザーの入力やナビゲーションに依存する一時的な状態などを保持するために使用します。そのため軽量なデータのみとし大量のデータの場合はPersistent storage(永続化ストレージ)を使用することが推奨されています。Saved instance stateは以下の種類があります。
- Jetpack Compose: rememberSaveable・・・Compose用
- View: onSaveInstanceState() API・・・主にUIロジックを保存
- ViewModel: SavedStateHandle・・・主にビジネスロジックを保存
ユーザーの期待値との乖離
実際にユーザーの期待とアプリの挙動が乖離を生まないようにするためにはユーザーの期待値とシステムの挙動を理解しておく必要があります。例えば以下のような動作を行った場合にはアプリはクリーンな状態から起動され直されることが期待値になり、以下の挙動ではシステムの挙動も一致してUIの状態をクリーンにします。
ユーザーがActivityを完全に終了する方法
- Activityをスワイプして履歴画面から削除する
- 設定画面でアプリを中止または強制終了する
- デバイスを再起動する
- なんらかの「終了」アクション(Activity.finishの呼び出し)を実行する
しかしユーザーがクリーンになることを期待しない場合でもシステムがUIの状態を破棄してしまうケースが存在します。
期待値に反してシステムがUIの状態を破棄してしまうケース
- 画面回転 / マルチウィンドウモードなどの構成変更
- アプリがバックグラウンド状態で他アプリのプロセスにより優先度が下がり破棄される
- etc...
これに応じたそれぞれの対応の違いは以下になります。
| ViewModel | Saved instance state | Persistent storage(永続ストレージ) | |
|---|---|---|---|
| 保存先 | メモリ内 | メモリ内 | ディスクまたはネットワーク上 |
| 構成変更後も保持 | ○ | ○ | ○ |
| システムによるプロセス終了後も保持 | × | ○ | ○ |
| ユーザーによるアクティビティ終了/onFinish() も保持 | × | × | ○ |
| データの上限 | 複雑なオブジェクトは問題ないが、使用可能なメモリによってスペースが制限される | プリミティブ型と String などのシンプルな小さなオブジェクトのみ | ネットワーク リソースからのディスク スペースまたはネットワークからの取得コスト / 時間によってのみ制限される |
| 読み取り / 書き込み時間 | 高速(メモリアクセスのみ) | 低速(シリアル化 / シリアル化解除が必要) | 低速(ディスク アクセスまたはネットワーク トランザクションが必要) |
今回はその中の1つである「Saved instance state」の「ViewModel: SavedStateHandle」の使い方についてまとめていきます。
SavedStateHandleの導入
公式リファレンス:ViewModel の保存済み状態モジュール
SavedStateHandleはViewModelの中で使用できる「Saved instance state」です。ViewModelだけでも構成の変更には耐え得る構造を実装することができますが、システムによるプロセス終了後の場合は値が消失してしまいます。これを防ぐためにViewModel内ではSavedStateHandleが用意されています。
SavedStateHandleはViewModelを継承したクラスのコンストラクタの引数として受け取ることが可能です。導入に必要な記述はこれのみでインスタンス化時などは特に意識する必要はありません。
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
使い方
SavedStateHandleはデータの取得・書込が行えるKey-Valueのマップ構造です。内部的な挙動はSavedInstanceStateなどと同じでBundleを使用してデータを保持しています。
データの取得・保存はset/getメソッドを使用します。
val id = savedStateHandle.get<String>("id")
savedStateHandle.set("id", userId)
またキーを直接指定することも可能です。
val id = savedStateHandle["id"] ?: ""
savedStateHandle["id"] = userId
復帰した際にシームレスに変更を通知するための手段としてLiveDataやStateFlowとの連携も可能になっています。
val filteredData: LiveData<List<String>> =
savedStateHandle.getLiveData<String>("query").switchMap { query ->
repository.getFilteredData(query)
}
val filteredData: StateFlow<List<String>> =
savedStateHandle.getStateFlow<String>("query")
.flatMapLatest { query ->
repository.getFilteredData(query)
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。







