Moduł zapisa stanu dla ViewModel Część Android Jetpack.
Jak wspomniano w artykule Zapisywanie stanów interfejsu użytkownika, obiekty ViewModel
mogą obsługiwać zmiany konfiguracji, więc nie musisz się martwić o stan w przypadku rotacji ani innych sytuacji. Jeśli jednak musisz obsłużyć proces zainicjowany przez system, możesz użyć interfejsu API SavedStateHandle
jako zapasowego.
Stan interfejsu jest zwykle przechowywany lub odwołuje się do niego w obiektach ViewModel
, a nie w czynnościach, więc użycie onSaveInstanceState()
lub rememberSaveable
wymaga użycia szablonu, którym może się zająć moduł zapisanego stanu.
Gdy używasz tego modułu, obiekty ViewModel
otrzymują obiekt SavedStateHandle
za pomocą konstruktora. Ten obiekt to mapa klucz-wartość, która umożliwia zapisywanie i pobieranie obiektów do i z zapisanego stanu. Te wartości są zachowywane po zakończeniu procesu przez system i pozostają dostępne w tym samym obiekcie.
Zapisane stany są powiązane z grupą zadań. Jeśli stos zadań zniknie, zapisany stan również zniknie. Może się tak zdarzyć, gdy wymusisz zatrzymanie aplikacji, usuniesz aplikację z menu Ostatnie lub zrestartujesz urządzenie. W takich przypadkach stos zadań znika i nie można przywrócić zapisanych informacji. W przypadku zakończonego przez użytkownika stanu interfejsu użytkownika zapisany stan nie jest przywracany. W przypadku scenariuszy inicjowanych przez system jest to możliwe.
Konfiguracja
Począwszy od Fragmentu 1.2.0 lub jego zależności transitive dependencyActivity 1.1.0, możesz przyjmować SavedStateHandle
jako argument konstruktora w ViewModel
.
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
Następnie możesz pobrać instancję ViewModel
bez dodatkowej konfiguracji. Domyślna fabryka ViewModel
zapewnia odpowiednie SavedStateHandle
dla Twojego 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); ... } ... }
Jeśli udostępniasz niestandardową instancję ViewModelProvider.Factory
, możesz włączyć użycie SavedStateHandle
, rozszerzając AbstractSavedStateViewModelFactory
.
Praca z SavedStateHandle
Klasa SavedStateHandle
to mapa klucz-wartość, która umożliwia zapisywanie i pobieranie danych do i z zapisanego stanu za pomocą metod set()
i get()
.
Dzięki funkcji SavedStateHandle
wartość zapytania jest zachowywana po zakończeniu procesu, co zapewnia, że użytkownik widzi ten sam zestaw przefiltrowanych danych przed i po jego utworzeniu, bez konieczności ręcznego zapisywania, przywracania i przesyłania tej wartości z powrotem do funkcji ViewModel
.
SavedStateHandle
ma też inne metody, których można się spodziewać podczas interakcji z mapą klucz-wartość:
contains(String key)
– sprawdza, czy podany klucz ma wartość.remove(String key)
– usuwa wartość dla danego klucza.keys()
– zwraca wszystkie klucze zawarte wSavedStateHandle
.
Dodatkowo możesz pobierać wartości z SavedStateHandle
za pomocą obserwowalnego uchwytu danych. Lista obsługiwanych typów:
LiveData
Pobieraj wartości z SavedStateHandle
, które są zawinięte w LiveData
observable za pomocą getLiveData()
.
Gdy wartość klucza zostanie zaktualizowana, parametr LiveData
otrzyma nową wartość. Najczęściej wartość jest ustawiana w wyniku interakcji użytkownika, np. wpisania zapytania w celu odfiltrowania listy danych. Ta zaktualizowana wartość może być następnie użyta do przekształcenia 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
Pobieraj wartości z SavedStateHandle
, które są zawinięte w StateFlow
observable za pomocą getStateFlow()
.
Gdy zaktualizujesz wartość klucza, parametr StateFlow
otrzyma nową wartość. Najczęściej wartość jest ustawiana w reakcji na interakcje użytkownika, np. wpisanie zapytania w celu odfiltrowania listy danych. Następnie możesz przekształcić tę zaktualizowaną wartość za pomocą innych operatorów przepływu danych.
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 } }
Obsługa stanu w eksperymentalnej wersji Compose
Element lifecycle-viewmodel-compose
udostępnia eksperymentalne interfejsy API saveable
, które umożliwiają współdziałanie usługi SavedStateHandle
z interfejsem Compose Saver
. Dzięki temu wszystkie elementy State
, które możesz zapisać za pomocą interfejsu rememberSaveable
z niestandardowym interfejsem Saver
, można też zapisać za pomocą interfejsu 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 } } }
Obsługiwane typy
Dane przechowywane w SavedStateHandle
są zapisywane i przywracane jako Bundle
wraz z resztą savedInstanceState
dla aktywności lub fragmentu.
Typy obsługiwane bezpośrednio
Domyślnie możesz wywoływać metody set()
i get()
w obiekcie SavedStateHandle
, aby uzyskać te same typy danych co w obiekcie Bundle
, jak pokazano poniżej:
Obsługa typu/klasy | Obsługa tablic |
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+) |
Jeśli klasa nie rozszerza żadnej z klas na liście powyżej, rozważ uczynienie jej możliwą do zapakowania przez dodanie adnotacji Kotlina @Parcelize
lub bezpośrednio zaimplementowanie Parcelable
.
Zapisywanie klas, których nie można podzielić
Jeśli klasa nie implementuje interfejsów Parcelable
ani Serializable
i nie można jej zmodyfikować, aby implementowała jeden z tych interfejsów, nie można bezpośrednio zapisać instancji tej klasy w klasie SavedStateHandle
.
Począwszy od wersji Lifecycle 2.3.0-alpha03 funkcja SavedStateHandle
pozwala zapisywać dowolne obiekty, podając własną logikę zapisywania i przywracania obiektu jako Bundle
za pomocą metody setSavedStateProvider()
. SavedStateRegistry.SavedStateProvider
to interfejs definiujący jedną metodę saveState()
zwracającą Bundle
zawierającą stan, który chcesz zapisać. Gdy SavedStateHandle
jest gotowy do zapisania swojego stanu, wywołuje funkcję saveState()
, aby pobrać element Bundle
z elementu SavedStateProvider
, i zapisuje element Bundle
dla powiązanego klucza.
Rozważ przykład aplikacji, która prosi o obraz z aplikacji aparatu za pomocą intencji ACTION_IMAGE_CAPTURE
, przekazując tymczasowy plik, w którym aparat ma zapisać obraz. Funkcja TempFileViewModel
zawiera logikę tworzenia tymczasowego pliku.
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; } }
Aby mieć pewność, że plik tymczasowy nie zostanie utracony, jeśli proces aktywności zostanie przerwany, a następnie przywrócony, usługa TempFileViewModel
może używać funkcji SavedStateHandle
do przechowywania danych. Aby umożliwić TempFileViewModel
zapisywanie danych, wprowadź SavedStateProvider
i ustaw go jako dostawcę w 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; } } }
Aby przywrócić dane File
, gdy użytkownik wróci, pobierz temp_file
Bundle
z poziomu SavedStateHandle
. To te same Bundle
, które udostępnia saveTempFile()
i zawiera ścieżkę bezwzględną. Ścieżka bezwzględna może być następnie użyta do utworzenia instancji 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 w testach
Aby przetestować ViewModel
, który ma zależność od SavedStateHandle
, utwórz nową instancję SavedStateHandle
z wymaganymi wartościami testowymi i przekaż ją do testowanej instancji ViewModel
.
Kotlin
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
Dodatkowe materiały
Więcej informacji o module Zapisane stany w przypadku ViewModel
znajdziesz w tych materiałach.
Ćwiczenia z programowania
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Zapisywanie stanów interfejsu
- Praca z obserwowalnymi obiektami danych
- Tworzenie widoków modelu z zależностями