Moduł Saved State dla ViewModel (Views)   Część Androida Jetpack.

Koncepcje i implementacja w Jetpack Compose

Jak wspomnieliśmy w artykule Zapisywanie stanów interfejsu, obiekty ViewModel mogą obsługiwać zmiany konfiguracji, więc nie musisz się martwić o stan w przypadku obrotu ekranu ani w innych sytuacjach. Jeśli jednak musisz obsługiwać zakończenie procesu zainicjowane przez system, możesz użyć interfejsu SavedStateHandle API jako kopii zapasowej.

Stan interfejsu jest zwykle przechowywany w obiektach ViewModel lub do nich odwoływany, a nie w aktywnościach, więc użycie onSaveInstanceState() wymaga pewnego kodu, który może obsłużyć moduł saved state.

Gdy używasz tego modułu, ViewModel obiekty 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 za pomocą tego samego obiektu.

Saved state jest powiązany ze stosem zadań. Jeśli stos zadań zniknie, zniknie też saved state. Może się to zdarzyć, gdy wymusisz zatrzymanie aplikacji, usuniesz ją z menu ostatnich aplikacji lub ponownie uruchomisz urządzenie. W takich przypadkach stos zadań znika i nie można przywrócić informacji w saved state. W scenariuszach odrzucenia stanu interfejsu zainicjowanych przez użytkownika saved state nie jest przywracany. W scenariuszach zainicjowanych przez system jest.

Konfiguracja

Począwszy od Fragment 1.2.0 lub jego zależności przechodniej Activity 1.1.0, możesz zaakceptować 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 udostępnia odpowiedni SavedStateHandle do 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);

        ...

    }

    ...
}

Podczas udostępniania niestandardowej instancji 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 użyciu SavedStateHandle wartość zapytania jest zachowywana po śmierci procesu, co zapewnia, że użytkownik widzi ten sam zestaw przefiltrowanych danych przed i po ponownym utworzeniu bez konieczności ręcznego zapisywania, przywracania i przekazywania tej wartości z powrotem do ViewModel przez aktywność lub fragment.

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

Dodatkowo możesz pobierać wartości z SavedStateHandle za pomocą obserwowalnego kontenera danych. Lista obsługiwanych typów:

LiveData

Pobieraj wartości z SavedStateHandle, które są opakowane w LiveData obserwowalny obiekt za pomocą getLiveData(). Gdy wartość klucza zostanie zaktualizowana, LiveData otrzyma nową wartość. Najczęściej wartość jest ustawiana w wyniku interakcji użytkownika, np. wpisania zapytania w celu przefiltrowania listy danych. Ta zaktualizowana wartość może być następnie używana do przekształcania 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);
    }
}

Obsługiwane typy

Dane przechowywane w SavedStateHandle są zapisywane i przywracane jako Bundle, wraz z resztą savedInstanceState dla aktywności lub fragmentu.

Zapisywanie klas niepodlegających serializacji

Jeśli klasa nie implementuje Parcelable ani Serializable i nie można jej zmodyfikować tak, aby implementowała jeden z tych interfejsów, nie można bezpośrednio zapisać instancji tej klasy w SavedStateHandle.

Począwszy od Lifecycle 2.3.0-alpha03, SavedStateHandle umożliwia zapisywanie dowolnego obiektu przez udostępnienie własnej logiki zapisywania i przywracania obiektu jako Bundle za pomocą metody setSavedStateProvider(). SavedStateRegistry.SavedStateProvider to interfejs, który definiuje pojedynczą metodę saveState(), która zwraca Bundle zawierający stan do zapisania. Gdy SavedStateHandle jest gotowy do zapisania swojego stanu, wywołuje saveState(), aby pobrać Bundle z SavedStateProvider, i zapisuje Bundle dla powiązanego klucza.

Rozważmy przykład aplikacji, która wysyła żądanie obrazu z aplikacji aparatu za pomocą intencji ACTION_IMAGE_CAPTURE, przekazując tymczasowy plik, w którym aparat ma przechowywać obraz. 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 mieć pewność, że plik tymczasowy nie zostanie utracony, jeśli proces aktywności zostanie zakończony i później przywrócony, TempFileViewModel może użyć SavedStateHandle do utrwalenia swoich danych. Aby umożliwić TempFileViewModel zapisywanie danych, zaimplementuj SavedStateProvider i ustaw go jako dostawcę w SavedStateHandle w 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 Bundle temp_file z SavedStateHandle. Jest to ten sam Bundle udostępniany przez saveTempFile(), który zawiera ścieżkę bezwzględną. Ścieżka bezwzględna może być następnie używana do utworzenia nowej 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;
        }
    }
}