ViewModel の保存済み状態のモジュール Android Jetpack の一部。
UI の状態の保存で説明したように、ViewModel オブジェクトは
構成の変更を処理できるため、ローテーション
などの状態を気にする必要はありません。ただし、システムによって開始されたプロセスの終了を処理する必要がある場合は、バックアップとして SavedStateHandle API を使用することをおすすめします。
UI の状態は通常、ViewModel オブジェクトに保存されるか、このオブジェクトで参照されます。そのため Compose で
rememberSaveable を使用するには、
保存済み状態モジュールで処理できるボイラープレートが必要です。
このモジュールを使用すると、ViewModel オブジェクトはコンストラクタを介して SavedStateHandle
オブジェクトを受け取ります。このオブジェクトは、保存済み状態との間でオブジェクトの書き込みや取得を行えるようにする Key-Value マップです。これらの値は、システムによってプロセスが強制終了された後も保持され、同じオブジェクトを介して引き続き使用できます。
保存済み状態はタスクスタックに関連付けられます。タスクスタックがなくなると、保存済みの状態もなくなります。これは、アプリを強制停止する、[最近] メニューからアプリを削除する、デバイスを再起動するときに発生することがあります。その場合、タスクスタックが表示されなくなり、保存済みの状態の情報を復元できなくなります。ユーザーが開始する UI の状態の破棄シナリオでは、保存済みの状態は復元されません。システムが開始するシナリオでは復元されます。
複雑なデータや大量のデータの場合は、永続ローカル ストレージを使用する必要があります。設定
SavedStateHandle を使用するには、ViewModel のコンストラクタ引数として受け取ります。
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
その後、追加の構成なしでコンポーザブル内で ViewModel のインスタンスを取得できます。デフォルトの ViewModel Factory は、適切な SavedStateHandle を ViewModel に提供します。
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
カスタム ViewModelProvider.Factory インスタンスを提供する場合、
SavedStateHandle の使用を CreationExtras と
viewModelFactory DSL を使用して有効にできます。
SavedStateHandle を使用する
SavedStateHandle クラスは、set() メソッドおよび
get() メソッドを介して、保存済み状態との間でデータの書き込みや
取得を行えるようにする Key-Value マップです。
SavedStateHandle を使用すると、クエリ値がプロセスの終了後も保持されるため、再作成の前と後で同じフィルタ処理が施されたデータのセットがユーザーに表示されるようになります。アクティビティやフラグメントを手動で保存、復元し、その値を ViewModel に転送して戻す必要はありません。
SavedStateHandle は、ホスト Activity が停止したとき(アプリがバックグラウンドに送信されたときなど)にのみ、そこに書き込まれたデータを保存します。Activity が停止している間、SavedStateHandle への書き込みは、Activity が onStart の後に onStop を再度受信しない限り(アプリがフォアグラウンドに切り替えられた後、再びバックグラウンドに切り替えられた場合など)保存されません。SavedStateHandle では、Key-Value マップの操作で役立つ可能性のあるメソッドが他にも用意されています。
contains(String key)- 指定されたキーの値が存在するかどうかを確認します。remove(String key)- 指定されたキーの値を削除します。keys()-SavedStateHandleに含まれるすべてのキーを返します。
また、観測可能なデータホルダーを使用して SavedStateHandle から値を取得できます。サポートされている型は次のとおりです。
get() メソッドや set() メソッドと同じメカニズムも使用して保存、復元されます。Compose では、状態はアプリがバックグラウンドに移行したときにのみキャプチャされます。つまり、アプリがバックグラウンドで実行されている間も、SavedStateHandle から観測可能なデータホルダーの更新を続けられますが、もう一度フォアグラウンドになる前に、アプリのプロセスが強制終了されると、状態の更新がすべて失われる可能性があります。StateFlow
StateFlow オブザーバブルでラップされた SavedStateHandle から値を取得できます。値を直接ミューテーションする必要があるかどうかに応じて、読み取り専用ストリームとミュータブル ストリームのどちらかを選択できます。
getStateFlow(): 状態を読み取るだけでよい場合に使用します。SavedStateHandleの別の場所でキーの値を更新すると、StateFlow は新しい値を受け取ります。読み取り専用ストリームを公開し、Flow オペレータを使用して変換する場合に最適です。getMutableStateFlow(): 読み取りと書き込みの両方のアクセス権が必要な場合に使用します。返されたMutableStateFlowの.valueを更新すると、基盤となるSavedStateHandleが自動的に更新されるため、キーを手動で設定する必要はありません。
ほとんどの場合、これらの値は、データのリストをフィルタリングするためのクエリ入力など、ユーザー操作により更新されます。
getMutableStateFlow の SavedStateHandle のサポートは、
Lifecycle バージョン 2.9.0 で追加されました。class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { // Use getMutableStateFlow to read and write the query directly private val _query = savedStateHandle.getMutableStateFlow("query", "") val query: StateFlow= _query.asStateFlow() // Use getStateFlow if you only need a read-only stream to react to changes val filteredData: StateFlow<List > = query.flatMapLatest { repository.getFilteredData(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun setQuery(newQuery: String) { // Updating the MutableStateFlow automatically updates the SavedStateHandle _query.value = newQuery } }
KotlinX シリアル化のサポート
複雑な UI 状態の場合は、KotlinX シリアル化とともに saved プロパティ デリゲートを使用できます。このデリゲートを使用すると、カスタムの @Serializable データクラスを SavedStateHandle に直接保持できます。これにより、プロセスの終了後も ViewModel の状態が保持されるため、Compose UI は再作成時に状態をシームレスに復元できます。
これを使用するには、データクラスに @Serializable アノテーションを付け、ViewModel で saved デリゲートを使用します。
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel // Ensure you have the savedstate-ktx dependency import androidx.savedstate.serialization.saved import kotlinx.serialization.Serializable @Serializable data class UserFilterState( val searchQuery: String, val minAge: Int, val includeInactive: Boolean ) class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { // The state is automatically serialized to a Bundle on process death, // and deserialized upon recreation. var filterState by savedStateHandle.saved { UserFilterState(searchQuery = "", minAge = 18, includeInactive = false) } fun updateQuery(newQuery: String) { // Mutating the property automatically updates the underlying SavedStateHandle filterState = filterState.copy(searchQuery = newQuery) } }
saved デリゲートは遅延評価されます。プロパティが初めてアクセスされるまで、初期化ラムダを呼び出したり、SavedStateHandle に何も保存したりしません。Compose State のサポート
状態が KotlinX
シリアル化ではなく Compose の Saver API に依存している場合、lifecycle-viewmodel-compose アーティファクトは
saveable デリゲートを提供します。これにより、
SavedStateHandle と Compose's Saver の相互運用が可能になり、State なら、
save via rememberSaveable とカスタム Saver を使って保存できる なら、
SavedStateHandle を使って保存することもできるようになります。
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
サポートされているタイプ
SavedStateHandle 内に保持されるデータは、アプリの残りの savedInstanceState とともに、Bundle として保存、復元されます。
直接サポートされている型
デフォルトでは、以下に示すように、Bundle と同じデータ型の SavedStateHandle で set() と get() を呼び出すことができます。
| 型 / クラスのサポート | 配列のサポート |
double |
double[] |
int |
int[] |
long |
long[] |
String |
String[] |
byte |
byte[] |
char |
char[] |
CharSequence |
CharSequence[] |
float |
float[] |
Parcelable |
Parcelable[] |
Serializable |
Serializable[] |
short |
short[] |
SparseArray |
|
Binder |
|
Bundle |
|
ArrayList |
|
Size (only in API 21+) |
|
SizeF (only in API 21+) |
クラスが上記のリストのいずれも拡張しない場合は、クラスを Parcelable にすることを検討してください。これには、@Parcelize Kotlin アノテーションを追加するか、Parcelable を直接実装します。
非 Parcelable クラスを保存する
クラスが Parcelable や Serializable を実装しておらず、これらのインターフェースのいずれかを実装するように変更できない場合、そのクラスのインスタンスを SavedStateHandle に直接保存できません。
Lifecycle 2.3.0-alpha03 以降、SavedStateHandle では、オブジェクトを Bundle として保存 / 復元するための独自ロジックを提供することで、任意のオブジェクトを保存できるようになりました。setSavedStateProvider()SavedStateRegistry.SavedStateProvider は、保存する状態を含む Bundle を返す
単一の saveState() メソッドを定義するインターフェースです。SavedStateHandle は状態を保存できるようになると、saveState() を呼び出して SavedStateProvider から Bundle を取得し、関連するキーの Bundle を保存します。
`ACTION_IMAGE_CAPTURE` インテントを介してカメラアプリから画像をリクエストし、カメラが画像を保存する場所に関する一時ファイルを渡すアプリの例を考えます。ACTION_IMAGE_CAPTURETempFileViewModel は、この一時ファイルを作成するロジックをカプセル化します。
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
TempFileViewModel で SavedStateHandle を使用してデータを保持することで、アクティビティのプロセスの強制終了後に復元された際、一時ファイルが消失されないようにできます。TempFileViewModel でデータを保存できるようにするには、
SavedStateProvider を実装し、SavedStateHandle の
ViewModel でプロバイダとして設定します。
private fun File.saveTempFile() = bundleOf("path", absolutePath) class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
ユーザーが戻ったときに File データを復元するには、SavedStateHandle から temp_file
Bundle を取得します。これは、絶対パスを含む saveTempFile() により提供される Bundle と同じです。この絶対パスを使用して、新しい File をインスタンス化できます。
private fun File.saveTempFile() = bundleOf("path", absolutePath) private fun Bundle.restoreTempFile() = if (containsKey("path")) { File(getString("path")) } else { null } class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { val tempFileBundle = savedStateHandle.get<Bundle>("temp_file") if (tempFileBundle != null) { tempFile = tempFileBundle.restoreTempFile() } savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
テストで SavedStateHandle を使用する
SavedStateHandle を依存関係として受け取る ViewModel をテストするには、必要なテスト値で SavedStateHandle の新しいインスタンスを作成し、テストする ViewModel インスタンスに渡します。
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
参考情報
ViewModel の保存済み状態モジュールの詳細については、以下のリソースをご覧ください。
Codelab
Views のコンテンツ
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- UI の状態を保存する
- 監視可能なデータ オブジェクトの使用
- 依存関係を使用して ViewModel を作成する