Модуль Saved State для ViewModel   Входит в Android Jetpack.

Как упоминалось в разделе Сохранение состояний пользовательского интерфейса, объекты ViewModel могут обрабатывать изменения конфигурации, поэтому вам не нужно беспокоиться о состоянии при поворотах или в других случаях. Однако если вам нужно обрабатывать завершение процесса, инициированное системой, вы можете использовать API SavedStateHandle в качестве резервного.

Состояние пользовательского интерфейса обычно хранится в объектах ViewModel или на них есть ссылки. Поэтому для использования rememberSaveable в Compose требуется шаблонный код, который может обрабатывать модуль сохраненного состояния.

При использовании этого модуля объекты ViewModel получают объект SavedStateHandle через конструктор. Этот объект представляет собой карту ключ-значение, которая позволяет записывать и извлекать объекты в сохраненное состояние и из него. Эти значения сохраняются после того, как система завершает процесс, и остаются доступными через тот же объект.

Сохраненное состояние привязано к стеку задач. Если стек задач исчезнет, сохраненное состояние также исчезнет. Это может произойти, если принудительно остановить приложение, удалить его из меню недавно использованных или перезагрузить устройство. В таких случаях стек задач исчезает, и вы не можете восстановить информацию в сохраненном состоянии. В сценариях закрытия состояния пользовательского интерфейса по инициативе пользователя сохраненное состояние не восстанавливается. В сценариях, инициированных системой, – да.

Настроить

Чтобы использовать SavedStateHandle, примите его в качестве аргумента конструктора для ViewModel.

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

После этого вы сможете получить экземпляр ViewModel в composable-функциях без дополнительной настройки. Стандартная фабрика ViewModel предоставляет подходящий объект SavedStateHandle для вашего ViewModel.

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

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

При предоставлении пользовательского экземпляра ViewModelProvider.Factory вы можете включить использование SavedStateHandle с помощью CreationExtras и DSL viewModelFactory.

Работа с SavedStateHandle

Класс SavedStateHandle – это карта ключ-значение, которая позволяет записывать данные в сохраненное состояние и извлекать их из него с помощью методов set() и get().

При использовании SavedStateHandle значение запроса сохраняется при завершении процесса, благодаря чему пользователь видит тот же набор отфильтрованных данных до и после повторного создания без необходимости вручную сохранять, восстанавливать и пересылать это значение обратно в ViewModel.

В SavedStateHandle также есть другие методы, которые могут понадобиться при взаимодействии с картой пар "ключ-значение":

  • contains(String key) – проверяет, есть ли значение для заданного ключа.
  • remove(String key) – удаляет значение для заданного ключа.
  • keys() – возвращает все ключи, содержащиеся в SavedStateHandle.

Кроме того, вы можете получать значения из SavedStateHandle с помощью наблюдаемого держателя данных. Поддерживаются следующие типы:

StateFlow

Вы можете получать значения из SavedStateHandle, заключенные в наблюдаемый объект StateFlow. В зависимости от того, нужно ли вам изменять значение напрямую, вы можете выбрать поток только для чтения или изменяемый поток:

  • getStateFlow(): используйте, если вам нужно только прочитать состояние. Когда вы обновляете значение ключа в другом месте SavedStateHandle, StateFlow получает новое значение. Это идеальный вариант, когда вы хотите предоставить поток только для чтения и преобразовать его с помощью операторов Flow.
  • getMutableStateFlow(): используйте, если вам нужен доступ и на чтение, и на запись. Обновление .value возвращенного MutableStateFlow автоматически обновляет базовый SavedStateHandle, избавляя вас от необходимости вручную устанавливать ключ.

Чаще всего эти значения обновляются в результате действий пользователя, например ввода запроса для фильтрации списка данных.

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

Для сложного состояния пользовательского интерфейса вы можете использовать делегат свойства saved вместе с сериализацией KotlinX. Этот делегат позволяет сохранять пользовательские классы данных @Serializable непосредственно в SavedStateHandle. Это позволяет сохранить состояние ViewModel при завершении процесса, чтобы интерфейс Compose мог легко восстановить свое состояние при повторном создании.

Чтобы использовать его, добавьте к классу данных аннотацию @Serializable и используйте делегат saved в 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

Если ваше состояние зависит от API Compose Saver, а не от KotlinX Serialization, артефакт lifecycle-viewmodel-compose предоставляет делегат saveable. Это обеспечивает совместимость между SavedStateHandle и Saver в Compose, поэтому любой объект State, который можно сохранить с помощью rememberSaveable с настраиваемым Saver, также можно сохранить с помощью 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, сохраняются и восстанавливаются как Bundle вместе с остальными savedInstanceState вашего приложения.

Типы, поддерживаемые напрямую

По умолчанию вы можете вызывать set() и get() для SavedStateHandle с теми же типами данных, что и для Bundle, как показано ниже.

Поддержка типов и классов Поддержка массивов
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+)

Если класс не расширяет ни один из перечисленных выше, сделайте его сериализуемым, добавив аннотацию Kotlin @Parcelize или напрямую реализовав Parcelable.

Сохранение классов, не поддерживающих Parcelable

Если класс не реализует Parcelable или Serializable и не может быть изменен для реализации одного из этих интерфейсов, то сохранить экземпляр этого класса непосредственно в SavedStateHandle невозможно.

Начиная с версии Lifecycle 2.3.0-alpha03, SavedStateHandle позволяет сохранять любой объект, предоставляя собственную логику для сохранения и восстановления объекта в виде Bundle с помощью метода setSavedStateProvider(). SavedStateRegistry.SavedStateProvider – это интерфейс, который определяет один метод saveState(), возвращающий Bundle с состоянием, которое вы хотите сохранить. Когда SavedStateHandle будет готов сохранить свое состояние, он вызовет saveState(), чтобы получить Bundle из SavedStateProvider, и сохранит 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
        }
    }
}

Чтобы временный файл не был утерян, если процесс действия будет завершен и позже восстановлен, TempFileViewModel может использовать SavedStateHandle для сохранения данных. Чтобы разрешить TempFileViewModel сохранять данные, реализуйте SavedStateProvider и задайте его в качестве поставщика в SavedStateHandle для 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
        }
    }
}

Чтобы восстановить данные File, когда пользователь вернется, извлеките temp_file Bundle из SavedStateHandle. Это тот же Bundle, который предоставляется saveTempFile() и содержит абсолютный путь. Затем абсолютный путь можно использовать для создания нового экземпляра 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 в тестах

Чтобы протестировать ViewModel, который принимает SavedStateHandle в качестве зависимости, создайте новый экземпляр SavedStateHandle с необходимыми тестовыми значениями и передайте его тестируемому экземпляру ViewModel.

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

Дополнительные ресурсы

Дополнительную информацию о модуле сохраненного состояния для ViewModel можно найти в следующих ресурсах:

Практические работы

Просмотр контента