ViewModel の保存済み状態のモジュール Android Jetpack の一部。
UI の状態の保存で説明したように、ViewModel
オブジェクトは構成の変更を処理できるため、ローテーションなどの状態を気にする必要はありません。ただし、システムによって開始されたプロセスの終了を処理する必要がある場合は、バックアップとして SavedStateHandle
API を使用することをおすすめします。
UI の状態は通常、アクティビティではなく ViewModel
オブジェクトに保存されるか、このオブジェクトで参照されます。そのため onSaveInstanceState()
または rememberSaveable
を使用するには、保存済み状態モジュールで処理できるボイラープレートが必要です。
このモジュールを使用する場合、ViewModel
オブジェクトは、コンストラクタを介して SavedStateHandle
オブジェクトを受け取ります。このオブジェクトは、保存済み状態との間でオブジェクトの書き込みや取得を行えるようにする Key-Value マップです。これらの値は、システムによってプロセスが強制終了された後も保持され、同じオブジェクトを介して引き続き使用できます。
保存済み状態はタスクスタックに関連付けられます。タスクスタックがなくなると、保存済みの状態もなくなります。これは、アプリを強制停止する、[最近] メニューからアプリを削除する、デバイスを再起動するときに発生することがあります。その場合、タスクスタックが表示されなくなり、保存済みの状態の情報を復元できなくなります。ユーザーが開始する UI の状態の破棄シナリオでは、保存済みの状態は復元されません。システム開始のシナリオでは、復元されます。
セットアップ
Fragment 1.2.0 およびその推移的な依存関係 Activity 1.1.0 以降、ViewModel
のコンストラクタ引数として SavedStateHandle
を受け入れることができるようになりました。
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
その後、追加の構成なしで ViewModel
のインスタンスを取得できます。デフォルトの ViewModel
ファクトリにより、適切な SavedStateHandle
が ViewModel
に提供されます。
Kotlin
class MainFragment : Fragment() { val vm: SavedStateViewModel by viewModels() ... }
Java
class MainFragment extends Fragment { private SavedStateViewModel vm; public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { vm = new ViewModelProvider(this).get(SavedStateViewModel.class); ... } ... }
カスタム ViewModelProvider.Factory
インスタンスを提供する場合、AbstractSavedStateViewModelFactory
を拡張することによって SavedStateHandle
の使用を有効にできます。
SavedStateHandle を使用する
SavedStateHandle
クラスは、set()
メソッドおよび get()
メソッドを介して、保存済み状態との間でデータの書き込みや取得を行えるようにする Key-Value マップです。
SavedStateHandle
を使用すると、クエリ値がプロセスの終了後も保持されるため、再作成の前と後で同じフィルタ処理が施されたデータのセットがユーザーに表示されるようになります。アクティビティやフラグメントを手動で保存、復元し、その値を ViewModel
に転送して戻す必要はありません。
SavedStateHandle
では、Key-Value マップの操作で役立つ可能性のあるメソッドが他にも用意されています。
contains(String key)
- 指定されたキーの値の有無を確認します。remove(String key)
- 指定されたキーの値を削除します。keys()
-SavedStateHandle
内に含まれるすべてのキーを返します。
また、オブザーバブルなデータホルダーを使用して SavedStateHandle
から値を取得できます。サポートされている型は次のとおりです。
LiveData
getLiveData()
を使用して LiveData
オブザーバブル内にラップされている値を SavedStateHandle
から取得します。キーの値が更新されると、LiveData
は新しい値を受け取ります。この値はほとんどの場合、データのリストをフィルタリングするためのクエリ入力など、ユーザー操作により設定されます。この更新された値は、LiveData
の変換に使用できます。
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: LiveData<List<String>> = savedStateHandle.getLiveData<String>("query").switchMap { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle savedStateHandle; public LiveData<List<String>> filteredData; public SavedStateViewModel(SavedStateHandle savedStateHandle) { this.savedStateHandle = savedStateHandle; LiveData<String> queryLiveData = savedStateHandle.getLiveData("query"); filteredData = Transformations.switchMap(queryLiveData, query -> { return repository.getFilteredData(query); }); } public void setQuery(String query) { savedStateHandle.set("query", query); } }
StateFlow
getStateFlow()
を使用して StateFlow
オブザーバブル内にラップされている値を SavedStateHandle
から取得します。キーの値を更新すると、StateFlow
は新しい値を受け取ります。この値は通常、データのリストをフィルタリングするクエリの入力のような、ユーザー操作により設定されます。この更新された値は、他の Flow の演算子を使用して変換できます。
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: StateFlow<List<String>> = savedStateHandle.getStateFlow<String>("query") .flatMapLatest { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
試験運用版 Compose の State のサポート
lifecycle-viewmodel-compose
アーティファクトは、試験運用版の
saveable
SavedStateHandle
と Compose の相互運用性を実現する API
Saver
そのため、State
rememberSaveable
で保存可能
カスタムの Saver
は SavedStateHandle
でも保存できます。
Kotlin
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+) |
クラスが上記のリストのいずれも拡張しない場合は、@Parcelize
Kotlin アノテーションを追加するか、Parcelable
を直接実装して、クラスを Parcelable にすることを検討してください。
非 Parcelable クラスを保存する
クラスが Parcelable
や Serializable
を実装しておらず、これらのインターフェースのいずれかを実装するように変更できない場合、そのクラスのインスタンスを SavedStateHandle
に直接保存できません。
ライフサイクル 2.3.0-alpha03 以降、setSavedStateProvider()
メソッドを使用して、オブジェクトを Bundle
として保存 / 復元するための独自ロジックを提供することで、SavedStateHandle
で任意のオブジェクトを保存できるようになりました。SavedStateRegistry.SavedStateProvider
は、保存する状態を含む Bundle
を返す単一の saveState()
メソッドを定義するインターフェースです。SavedStateHandle
は状態を保存できるようになると、saveState()
を呼び出して SavedStateProvider
から Bundle
を取得し、関連するキーの Bundle
を保存します。
ACTION_IMAGE_CAPTURE
インテントを介してカメラアプリから画像をリクエストし、カメラが画像を保存する場所に関する一時ファイルを渡すアプリの例を考えます。TempFileViewModel
は、この一時ファイルを作成するロジックをカプセル化します。
Kotlin
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel() { } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } }
TempFileViewModel
で SavedStateHandle
を使用してデータを保持することで、アクティビティのプロセスの強制終了後に復元された際、一時ファイルが消失されないようにできます。TempFileViewModel
でデータを保存できるようにするには、SavedStateProvider
を実装し、ViewModel
の SavedStateHandle
でプロバイダとして設定します。
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } } }
ユーザーが戻ったときに File
データを復元するには、SavedStateHandle
から temp_file
Bundle
を取得します。これは、絶対パスを含む saveTempFile()
により提供される Bundle
と同じです。この絶対パスを使用して、新しい File
をインスタンス化できます。
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { Bundle tempFileBundle = savedStateHandle.get("temp_file"); if (tempFileBundle != null) { tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle); } savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } @Nullable private static File restoreTempFile(Bundle bundle) { if (bundle.containsKey("path") { return File(bundle.getString("path")); } return null; } } }
テストで SavedStateHandle を使用する
SavedStateHandle
を依存関係として受け取る ViewModel
をテストするには、必要なテスト値で SavedStateHandle
の新しいインスタンスを作成し、テストする ViewModel
インスタンスに渡します。
Kotlin
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
参考情報
ViewModel
の保存済み状態モジュールの詳細については、以下のリソースをご覧ください。
Codelab
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- UI の状態を保存する
- 監視可能なデータ オブジェクトの使用
- 依存関係を使用して ViewModel を作成する