Модуль сохраненного состояния для ViewModel . Часть Android Jetpack .

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

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

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

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

Настраивать

Начиная с Fragment 1.2.0 или его транзитивной зависимости Activity 1.1.0 , вы можете принять SavedStateHandle в качестве аргумента конструктора для вашей ViewModel .

Котлин

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 вы можете включить использование SavedStateHandle , расширив AbstractSavedStateViewModelFactory .

Работа с SavedStateHandle

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

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

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

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

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

LiveData

Получите значения из SavedStateHandle , которые заключены в наблюдаемый LiveData , с помощью getLiveData() . Когда значение ключа обновляется, 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

Получите значения из SavedStateHandle , которые заключены в наблюдаемый StateFlow , с помощью getStateFlow() . Когда вы обновляете значение ключа, 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
    }
}

Государственная поддержка Experimental Compose

Артефакт lifecycle-viewmodel-compose предоставляет экспериментальные saveable API, которые обеспечивают взаимодействие между SavedStateHandle и Compose Saver , так что любое 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+)

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

Сохранение непарцеллируемых классов

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

Начиная с жизненного цикла 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
        }
    }
}

Ява

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 и установите его в качестве поставщика в 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
        }
    }
}

Ява

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 , когда пользователь вернется, извлеките Bundle temp_file из 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
      }
    }
}

Ява

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 в тестах

Чтобы протестировать 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 см. в следующих ресурсах.

Кодлабы

{% дословно %} {% дословно %} {% дословно %} {% дословно %}