Модуль Saved State для ViewModel (Views)   Часть Android Jetpack.

Основные понятия и реализация Jetpack Compose

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

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

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

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

Настроить

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

Kotlin

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

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        state = savedStateHandle;
    }

    ...
}

После этого вы сможете получить экземпляр ViewModel без дополнительной настройки. Стандартная фабрика ViewModel предоставляет подходящий экземпляр SavedStateHandle для вашего 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);

        ...

    }

    ...
}

Если вы используете собственный экземпляр 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.

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

Поддерживаемые типы

Данные, хранящиеся в SavedStateHandle, сохраняются и восстанавливаются как Bundle вместе с остальными savedInstanceState для действия или фрагмента.

Сохранение классов, не поддерживающих 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 содержит логику для создания этого временного файла.

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

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

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