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
を受け入れることができるようになりました。
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
その後、追加の構成なしで ViewModel
のインスタンスを取得できます。デフォルトの ViewModel
ファクトリにより、適切な SavedStateHandle
が ViewModel
に提供されます。
class MainFragment : Fragment() { val vm: SavedStateViewModel by viewModels() ... }
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
の変換に使用できます。
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 } }
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 の演算子を使用して変換できます。
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
API を提供します。この API を使用すると、SavedStateHandle
と Compose の Saver
の相互運用が可能になり、rememberSaveable
とカスタム Saver
を使って保存できる State
なら、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+) |
クラスが上記のリストのいずれも拡張しない場合は、@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
は、この一時ファイルを作成するロジックをカプセル化します。
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
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
でプロバイダとして設定します。
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 } } }
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
をインスタンス化できます。
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 } } }
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
インスタンスに渡します。
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
参考情報
ViewModel
の保存済み状態モジュールの詳細については、以下のリソースをご覧ください。
Codelab
あなたへのおすすめ
UI の状態を保存する
構成変更の前後で UI の状態を保存する方法をご確認ください
監視可能なデータ オブジェクトを操作する
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.
依存関係を使用して ViewModel を作成する
ViewModel を使用すると、ライフサイクルを意識した方法で UI のデータを管理できます。