Moduł Zapisany stan dla ViewModel Część stanowiąca część Androida Jetpack.

Jak wspomnieliśmy w sekcji Zapisywanie stanów interfejsu, obiekty ViewModel mogą obsługiwać zmiany konfiguracji, więc nie musisz się martwić o stan w rotacjach ani w innych przypadkach. Jeśli jednak musisz radzić sobie ze śmiercią procesów inicjowanych przez system, możesz użyć interfejsu API SavedStateHandle jako opcji zapasowej.

Stan interfejsu użytkownika jest zwykle przechowywany lub do którego odwołują się obiekty ViewModel, a nie aktywności, więc użycie onSaveInstanceState() lub rememberSaveable wymaga pewnego schematu, który może obsługiwać zapisany moduł stanu.

Gdy używany jest ten moduł, obiekty ViewModel otrzymują obiekt SavedStateHandle za pomocą swojego konstruktora. Ten obiekt jest mapą klucz-wartość, która umożliwia zapisywanie obiektów w zapisanym stanie i ich pobieranie. Wartości te pozostają aktywne po zakończeniu procesu przez system i pozostają dostępne w tym samym obiekcie.

Zapisany stan jest powiązany ze stosem zadań. Jeśli stos zadań zniknie, zapisany stan również zniknie. Może się to zdarzyć, gdy wymuszasz zatrzymanie aplikacji, została ona usunięta z menu ostatnich lub gdy urządzenie zostało ponownie uruchomione. W takich przypadkach stos zadań zniknie i nie będzie można przywrócić informacji w zapisanym stanie. W scenariuszach odrzucania stanu interfejsu inicjowanego przez użytkownika zapisany stan nie jest przywracany. W sytuacjach inicjowanych przez system tak właśnie jest.

Skonfiguruj

Począwszy od fragmentu 1.2.0 lub jego pośredniej zależności, czynności 1.1.0 możesz akceptować SavedStateHandle jako argument konstruktora dla ViewModel.

Kotlin

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

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

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

    ...
}

Potem możesz pobrać instancję ViewModel bez dodatkowej konfiguracji. Domyślna fabryka ViewModel udostępnia na urządzeniu ViewModel odpowiednie SavedStateHandle.

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

        ...


    }

    ...
}

Udostępniając niestandardową instancję ViewModelProvider.Factory, możesz włączyć użycie SavedStateHandle, rozszerzając zakres AbstractSavedStateViewModelFactory.

Praca z SavedStateHandle

Klasa SavedStateHandle to mapa klucz-wartość, która umożliwia zapisywanie i pobieranie danych do zapisanego stanu i z niego za pomocą metod set() i get().

Gdy używany jest SavedStateHandle, wartość zapytania jest zachowywana przez cały proces zakończenia procesu, dzięki czemu użytkownik widzi ten sam zbiór odfiltrowanych danych przed i po użyciu funkcji, bez konieczności ręcznego zapisywania, przywracania i przekazywania tej wartości do funkcji ViewModel.

SavedStateHandle oferuje też inne metody, których możesz się spodziewać podczas interakcji z mapą klucz-wartość:

Dodatkowo możesz pobierać wartości z funkcji SavedStateHandle za pomocą możliwego do obserwacji właściciela danych. Lista obsługiwanych typów:

LiveData,

Pobierz z SavedStateHandle wartości, które są umieszczone w obiekcie LiveData możliwym do obserwacji za pomocą getLiveData(). Po zaktualizowaniu wartości klucza LiveData otrzyma nową wartość. Wartość jest często ustawiana na podstawie interakcji użytkownika, takich jak wpisanie zapytania w celu odfiltrowania listy danych. Za pomocą zaktualizowanej wartości możesz przekształcić element 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,

Pobierz z SavedStateHandle wartości, które są umieszczone w obiekcie StateFlow możliwym do obserwacji za pomocą getStateFlow(). Po zaktualizowaniu wartości klucza StateFlow otrzyma nową wartość. Wartość ta jest często ustawiana na podstawie interakcji użytkownika, takich jak wpisanie zapytania w celu filtrowania listy danych. Następnie możesz przekształcić tę zaktualizowaną wartość za pomocą innych operatorów przepływu.

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 tworzenia wiadomości eksperymentalnych

Artefakt lifecycle-viewmodel-compose udostępnia eksperymentalne interfejsy API saveable umożliwiające interoperacyjność między interfejsami SavedStateHandle i Saver. Dzięki temu wszystkie adresy State, które możesz zapisać za pomocą rememberSaveable z niestandardową wartością Saver, można zapisać w 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 elemencie SavedStateHandle są zapisywane i przywracane jako Bundle razem z resztą savedInstanceState na potrzeby aktywności lub fragmentu.

Typy obsługiwane bezpośrednio

Domyślnie możesz wywołać set() i get() w SavedStateHandle dla tych samych typów danych co Bundle, jak pokazano poniżej:

Obsługa typu/klasy Obsługa tablicy
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 żadnego z elementów z listy powyżej, rozważ przekształcenie jej w funkcję parcelową przez dodanie adnotacji Kotlin @Parcelize lub bezpośrednie wdrożenie Parcelable.

Zapisywanie klas bez możliwości wynajęcia

Jeśli klasa nie implementuje Parcelable ani Serializable i nie można jej zmodyfikować w celu zaimplementowania jednego z tych interfejsów, nie można bezpośrednio zapisać instancji tej klasy w SavedStateHandle.

Począwszy od cyklu życia 2.3.0-alpha03, SavedStateHandle umożliwia zapisywanie dowolnego obiektu, udostępniając własną logikę zapisu i przywracania obiektu jako Bundle przy użyciu metody setSavedStateProvider(). SavedStateRegistry.SavedStateProvider to interfejs określający pojedynczą metodę saveState() zwracającą obiekt Bundle zawierający stan, który chcesz zapisać. Gdy SavedStateHandle jest gotowy do zapisania swojego stanu, wywołuje metodę saveState(), aby pobrać Bundle z SavedStateProvider i zapisuje Bundle dla powiązanego klucza.

Przeanalizujmy przykład aplikacji, która za pomocą intencji ACTION_IMAGE_CAPTURE wysyła do aplikacji aparatu żądanie udostępnienia obrazu i przekazuje plik tymczasowy miejsca, w którym kamera ma zapisać obraz. Pole TempFileViewModel zawiera logikę tworzenia tego pliku tymczasowego.

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 plik tymczasowy nie został utracony, gdy proces aktywności zostanie przerwany, a później przywrócony, TempFileViewModel może zachować dane za pomocą metody SavedStateHandle. Aby zezwolić usłudze TempFileViewModel na zapisywanie danych, zaimplementuj SavedStateProvider i ustaw ją jako dostawcę w SavedStateHandle elementu 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 po powrocie użytkownika, pobierz temp_file Bundle z SavedStateHandle. Jest to ta sama wartość Bundle podana przez funkcję saveTempFile(), która zawiera ścieżkę bezwzględną. Ścieżkę bezwzględną można następnie użyć do utworzenia instancji nowego obiektu 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 wymaga SavedStateHandle jako zależności, 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 Zapisanego stanu dla ViewModel znajdziesz w tych materiałach.

Ćwiczenia z programowania