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, so using rememberSaveable in Compose 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

To use SavedStateHandle, accept it as a constructor argument to your ViewModel.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

You can then retrieve an instance of your ViewModel within your composables without any additional configuration. The default ViewModel factory provides the appropriate SavedStateHandle to your ViewModel.

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

When providing a custom ViewModelProvider.Factory instance, you can enable usage of SavedStateHandle by using CreationExtras and the viewModelFactory DSL.

Working with SavedStateHandle

The SavedStateHandle class is a key-value map that lets you 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:

Additionally, you can retrieve values from SavedStateHandle using an observable data holder. The list of supported types includes the following:

StateFlow

You can retrieve values from SavedStateHandle wrapped in a StateFlow observable. Depending on whether you need to mutate the value directly, you can choose between a read-only or mutable stream:

  • getStateFlow(): Use this if you only need to read the state. When you update the key's value elsewhere in the SavedStateHandle, the StateFlow receives the new value. This is ideal when you want to expose a read-only stream and transform it using Flow operators.
  • getMutableStateFlow(): Use this if you need both read and write access. Updating the .value of the returned MutableStateFlow automatically updates the underlying SavedStateHandle, saving you from needing to manually set the key.

Most often, you update these values due to user interactions, such as entering a query to filter a list of data.

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 Serialization support

For complex UI state, you can use the saved property delegate alongside KotlinX Serialization. This delegate lets you persist custom @Serializable data classes directly into the SavedStateHandle. This preserves your ViewModel's state across process death, so your Compose UI can seamlessly restore its state upon recreation.

To use it, annotate your data class with @Serializable and use the saved delegate in your ViewModel:

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)
    }
}

Compose State support

If your state relies on Compose's Saver APIs instead of on KotlinX Serialization, the lifecycle-viewmodel-compose artifact provides the saveable delegate. This allows 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.

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 your app.

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 lets you 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.

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

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:

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
        }
    }
}

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.

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 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.

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

Views content