Saved State module for ViewModel Part of Android Jetpack.
As mentioned in
Saving UI States,
ViewModel
objects can handle
configuration changes, so you don't need to worry about state in rotations
or other cases. However, if you need to handle system-initiated process
death, you might want to use the SavedStateHandle
API as backup.
UI state is usually stored or referenced in ViewModel
objects and not
activities, so using onSaveInstanceState()
or rememberSaveable
requires some
boilerplate that the saved state module
can handle for you.
When using this module, ViewModel
objects receive a
SavedStateHandle
object
through its constructor. This object is a key-value map that lets you
write and retrieve objects to and from the saved state. These values
persist after the process is killed by the system and remain available
through the same object.
Saved state is tied to your task stack. If your task stack goes away, your saved state also goes away. This can occur when force stopping an app, removing the app from the recents menu, or rebooting the device. In such cases, the task stack disappears and you can't restore the information in saved state. In User-initiated UI state dismissal scenarios, saved state isn't restored. In system-initiated scenarios, it is.
Setup
Beginning with Fragment 1.2.0
or its transitive dependency
Activity 1.1.0, you can accept
a SavedStateHandle
as a constructor argument to your ViewModel
.
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
You can then retrieve an instance of your ViewModel
without any additional
configuration. The default ViewModel
factory provides the appropriate
SavedStateHandle
to your 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); ... } ... }
When providing a custom
ViewModelProvider.Factory
instance, you can enable usage of SavedStateHandle
by extending
AbstractSavedStateViewModelFactory
.
Working with SavedStateHandle
The SavedStateHandle
class is a key-value map that allows you to write and
retrieve data to and from the saved state through the
set()
and get()
methods.
By using SavedStateHandle
, the query value is retained across process death,
ensuring that the user sees the same set of filtered data before and after
recreation without the activity or fragment needing to manually save, restore,
and forward that value back to the ViewModel
.
SavedStateHandle
also has other methods you might expect when interacting
with a key-value map:
contains(String key)
- Checks if there is a value for the given key.remove(String key)
- Removes the value for the given key.keys()
- Returns all keys contained within theSavedStateHandle
.
Additionally, you can retrieve values from SavedStateHandle
using an
observable data holder. The list of supported types are:
LiveData
Retrieve values from SavedStateHandle
that are wrapped in a
LiveData
observable using
getLiveData()
.
When the key's value is updated, the LiveData
receives the new value. Most
often, the value is set due to user interactions, such as entering a query to
filter a list of data. This updated value can then be used to
transform 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
Retrieve values from SavedStateHandle
that are wrapped in a
StateFlow
observable using
getStateFlow()
.
When you update the key's value, the StateFlow
receives the new value. Most
often, you might set the value due to user interactions, such as entering a
query to filter a list of data. You can then transform this updated value
using other Flow operators.
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 } }
Experimental Compose's State support
The lifecycle-viewmodel-compose
artifact provides the experimental
saveable
APIs that allow interoperability between SavedStateHandle
and Compose's
Saver
so that any State
that you
can save via rememberSaveable
with a custom Saver
can also be saved with 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 } } }
Supported types
Data kept within a SavedStateHandle
is saved and restored as a
Bundle
, along with the rest of the
savedInstanceState
for the activity or fragment.
Directly supported types
By default, you can call set()
and get()
on a SavedStateHandle
for the
same data types as a Bundle
, as shown below:
Type/Class support | Array support |
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+) |
If the class does not extend one of those in the above list, consider making
the class parcelable by adding the @Parcelize
Kotlin annotation or implementing
Parcelable
directly.
Saving non-parcelable classes
If a class does not implement Parcelable
or Serializable
and cannot be
modified to implement one of those interfaces, then it is not possible to
directly save an instance of that class into a SavedStateHandle
.
Beginning with
Lifecycle 2.3.0-alpha03,
SavedStateHandle
allows you to save any object by providing your own
logic for saving and restoring your object as a
Bundle
using the
setSavedStateProvider()
method. SavedStateRegistry.SavedStateProvider
is an interface that defines a single
saveState()
method that returns a Bundle
containing the state you want to save. When
SavedStateHandle
is ready to save its state, it calls saveState()
to retrieve the Bundle
from the SavedStateProvider
and saves the
Bundle
for the associated key.
Consider an example of an app that requests an image from the camera app via
the ACTION_IMAGE_CAPTURE
intent, passing in a temporary file for where the camera should store the
image. The TempFileViewModel
encapsulates the logic for creating that
temporary file.
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; } }
To ensure the temporary file is not lost if the activity's process is killed
and later restored, TempFileViewModel
can use the SavedStateHandle
to
persist its data. To allow TempFileViewModel
to save its data, implement
SavedStateProvider
and set it as a provider on the SavedStateHandle
of
the ViewModel
:
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; } } }
To restore the File
data when the user returns, retrieve the temp_file
Bundle
from the SavedStateHandle
. This is the same Bundle
provided by
saveTempFile()
that contains the absolute path. The absolute path can then
be used to instantiate a new 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 in tests
To test a ViewModel
that takes a SavedStateHandle
as a dependency, create
a new instance of SavedStateHandle
with the test values it requires and pass
it to the ViewModel
instance you are testing.
Kotlin
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
Additional resources
For further information about the Saved State module for ViewModel
, see the
following resources.
Codelabs
Recommended for you
- Note: link text is displayed when JavaScript is off
- Save UI states
- Work with observable data objects
- Create ViewModels with dependencies