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()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ść:

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()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