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:

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