Zapisywanie stanów interfejsu (widoków)

Koncepcje i implementacja Jetpack Compose

W tym przewodniku omawiamy oczekiwania użytkowników dotyczące stanu interfejsu oraz opcje zachowania stanu.

Zapisywanie i przywracanie stanu interfejsu aktywności szybko po tym, jak system zniszczy aktywności lub aplikacje, jest niezbędne do zapewnienia dobrego wrażenia użytkownika. Użytkownicy oczekują, że stan interfejsu pozostanie taki sam, ale system może zniszczyć aktywność i jej zapisany stan.

Aby zniwelować różnicę między oczekiwaniami użytkowników a zachowaniem systemu, użyj kombinacji tych metod:

  • ViewModel obiekty.
  • Zapisane stany instancji w tych kontekstach:
  • Pamięć lokalna do utrwalania stanu interfejsu podczas przejść między aplikacjami i aktywnościami.

Optymalne rozwiązanie zależy od złożoności danych interfejsu, przypadków użycia aplikacji oraz od znalezienia równowagi między szybkością dostępu do danych a wykorzystaniem pamięci.

Upewnij się, że Twoja aplikacja spełnia oczekiwania użytkowników i oferuje szybki, responsywny interfejs. Unikaj opóźnień podczas wczytywania danych do interfejsu, zwłaszcza po typowych zmianach konfiguracji, takich jak obrót.

Oczekiwania użytkowników i zachowanie systemu

W zależności od działania użytkownika oczekuje on, że stan działania zostanie wyczyszczony lub zachowany. W niektórych przypadkach system automatycznie robi to, czego oczekuje użytkownik. W innych przypadkach system robi coś przeciwnego.

Odrzucanie stanu interfejsu zainicjowane przez użytkownika

Użytkownik oczekuje, że gdy uruchomi aktywność, jej tymczasowy stan interfejsu pozostanie taki sam, dopóki użytkownik całkowicie nie odrzuci aktywności. Użytkownik może całkowicie odrzucić aktywność, wykonując te czynności:

  • Przesunięcie aktywności z ekranu Przegląd (ostatnie).
  • Zatrzymanie lub wymuszenie zamknięcia aplikacji na ekranie Ustawienia.
  • Ponowne uruchomienie urządzenia.
  • Wykonanie jakiegoś działania „kończącego” (które jest obsługiwane przez Activity.finish()).

W tych przypadkach całkowitego odrzucenia użytkownik zakłada, że trwale opuścił aktywność, a jeśli ponownie ją otworzy, oczekuje, że aktywność rozpocznie się w czystym stanie. Podstawowe zachowanie systemu w tych scenariuszach odrzucenia jest zgodne z oczekiwaniami użytkownika – instancja aktywności zostanie zniszczona i usunięta z pamięci wraz z zapisanym w niej stanem i zapisanym stanem instancji powiązanym z aktywnością.

Istnieją pewne wyjątki od tej reguły dotyczącej całkowitego odrzucenia. Na przykład użytkownik może oczekiwać, że przeglądarka przeniesie go dokładnie do strony, którą oglądał, zanim zamknął przeglądarkę za pomocą przycisku Wstecz.

Odrzucanie stanu interfejsu zainicjowane przez system

Użytkownik oczekuje, że stan interfejsu aktywności pozostanie taki sam podczas zmiany konfiguracji, np. obrotu lub przełączenia do trybu wielu okien. Domyślnie jednak system niszczy aktywność, gdy nastąpi taka zmiana konfiguracji, usuwając wszelki stan interfejsu przechowywany w instancji aktywności. Aby dowiedzieć się więcej o konfiguracjach urządzeń, zobacz stronę referencyjną Konfiguracja.

Pamiętaj, że można (choć nie jest to zalecane) zastąpić domyślne zachowanie w przypadku zmian konfiguracji. Więcej informacji znajdziesz w artykule Obsługa zmiany konfiguracji.

Użytkownik oczekuje też, że stan interfejsu Twojej aktywności pozostanie taki sam, jeśli tymczasowo przełączy się na inną aplikację, a potem wróci do Twojej aplikacji. Na przykład użytkownik wyszukuje coś w Twojej aktywności związanej z wyszukiwaniem, a potem naciska przycisk strony głównej lub odbiera połączenie telefoniczne. Gdy wróci do aktywności związanej z wyszukiwaniem, oczekuje, że słowo kluczowe w sieci wyszukiwania i wyniki wyszukiwania będą nadal dostępne, dokładnie tak jak wcześniej.

W tym scenariuszu Twoja aplikacja jest umieszczana w tle, a system dokłada wszelkich starań, aby proces aplikacji pozostał w pamięci. System może jednak zniszczyć proces aplikacji, gdy użytkownik korzysta z innych aplikacji. W takim przypadku instancja aktywności jest niszczona wraz z zapisanym w niej stanem. Gdy użytkownik ponownie uruchomi aplikację, aktywność jest nieoczekiwanie w czystym stanie. Więcej informacji o śmierci procesu znajdziesz w artykule Procesy i cykl życia aplikacji.

Opcje zachowania stanu interfejsu

Gdy oczekiwania użytkownika dotyczące stanu interfejsu nie są zgodne z domyślnym zachowaniem systemu, musisz zapisać i przywrócić stan interfejsu użytkownika, aby zniszczenie zainicjowane przez system było dla użytkownika niewidoczne.

Każda z opcji zachowania stanu interfejsu różni się w tych wymiarach, które mają wpływ na wrażenia użytkownika:

ViewModel

Zapisany stan instancji

Pamięć trwała

Lokalizacja zapisu

w pamięci

w pamięci

na dysku lub w sieci

Przetrwa zmianę konfiguracji

Tak

Tak

Tak

Przetrwa śmierć procesu zainicjowaną przez system

Nie

Tak

Tak

Przetrwa całkowite odrzucenie aktywności przez użytkownika lub wywołanie funkcji finish()

Nie

Nie

Tak

Ograniczenia dotyczące danych

złożone obiekty są w porządku, ale miejsce jest ograniczone dostępną pamięcią

tylko w przypadku typów prostych i prostych, małych obiektów, takich jak String

ograniczone tylko miejscem na dysku lub kosztem / czasem pobierania z zasobu sieciowego

Czas odczytu i zapisu

szybki (tylko dostęp do pamięci)

wolny (wymaga serializacji/deserializacji)

wolny (wymaga dostępu do dysku lub transakcji sieciowej)

Używanie ViewModel do obsługi zmian konfiguracji

ViewModel idealnie nadaje się do przechowywania i zarządzania danymi związanymi z interfejsem, gdy użytkownik aktywnie korzysta z aplikacji. Umożliwia szybki dostęp do danych interfejsu i pomaga uniknąć ponownego pobierania danych z sieci lub dysku podczas obrotu, zmiany rozmiaru okna i innych często występujących zmian konfiguracji. Aby dowiedzieć się, jak zaimplementować a ViewModel, przeczytaj przewodnik ViewModel.

ViewModel przechowuje dane w pamięci, co oznacza, że ich pobieranie jest tańsze niż pobieranie danych z dysku lub sieci. ViewModel jest powiązany z aktywnością (lub innym właścicielem cyklu życia) – pozostaje w pamięci podczas zmiany konfiguracji, a system automatycznie kojarzy ViewModel z nową instancją aktywności, która jest wynikiem zmiany konfiguracji.

ViewModel są automatycznie niszczone przez system, gdy użytkownik wycofuje się z aktywności lub fragmentu albo gdy wywołasz funkcję finish(), co oznacza, że stan jest czyszczony zgodnie z oczekiwaniami użytkownika w tych scenariuszach.

W przeciwieństwie do zapisanego stanu instancji ViewModel są niszczone podczas śmierci procesu zainicjowanej przez system. Aby ponownie wczytać dane po śmierci procesu zainicjowanej przez system w ViewModel, użyj SavedStateHandle API. Jeśli dane są powiązane z interfejsem i nie muszą być przechowywane w ViewModel, użyj funkcji onSaveInstanceState(). Jeśli dane są danymi aplikacji, lepiej je utrwalić na dysku.

Jeśli masz już rozwiązanie w pamięci do przechowywania stanu interfejsu podczas zmian konfiguracji, być może nie musisz używać ViewModel.

Używanie zapisanego stanu instancji jako kopii zapasowej do obsługi śmierci procesu zainicjowanej przez system

The onSaveInstanceState() callback in the View system and SavedStateHandle in ViewModels store data needed to reload the state of a UI controller, such as an activity or a fragment, if the system destroys and later recreates that controller. Aby dowiedzieć się, jak zaimplementować zapisany stan instancji za pomocą funkcji onSaveInstanceState, przeczytaj sekcję Zapisywanie i przywracanie stanu działania w przewodniku Cykl życia działania.

Pakiety zapisanych stanów instancji są zachowywane zarówno podczas zmian konfiguracji, jak i śmierci procesu, ale są ograniczone przez miejsce na dane i szybkość, ponieważ różne interfejsy API serializują dane. Serializacja może zużywać dużo pamięci, jeśli serializowane obiekty są złożone. Ponieważ ten proces odbywa się w wątku głównym podczas zmiany konfiguracji, długotrwała serializacja może powodować utratę klatek i zacinanie się obrazu.

Nie używaj zapisanego stanu instancji do przechowywania dużych ilości danych, takich jak mapy bitowe, ani złożonych struktur danych, które wymagają długotrwałej serializacji lub deserializacji. Zamiast tego przechowuj tylko typy proste i proste, małe obiekty, takie jak String. Dlatego używaj zapisanego stanu instancji do przechowywania minimalnej ilości danych, np. identyfikatora, aby w razie awarii innych mechanizmów utrwalania danych można było odtworzyć dane niezbędne do przywrócenia interfejsu do poprzedniego stanu. Większość aplikacji powinna to zaimplementować, aby obsługiwać śmierć procesu zainicjowaną przez system.

W zależności od przypadków użycia aplikacji możesz w ogóle nie musieć używać zapisanego stanu instancji. Na przykład przeglądarka może przenieść użytkownika z powrotem do strony, którą oglądał, zanim zamknął przeglądarkę. Jeśli Twoja aktywność zachowuje się w ten sposób, możesz zrezygnować z używania zapisanego stanu instancji i zamiast tego utrwalać wszystko lokalnie.

Gdy otwierasz aktywność z intencji, pakiet dodatków jest dostarczany do aktywności zarówno podczas zmiany konfiguracji, jak i podczas przywracania aktywności przez system.

W obu tych scenariuszach nadal powinnaś używać ViewModel, aby uniknąć marnowania cykli na ponowne wczytywanie danych z bazy danych podczas zmiany konfiguracji.

W przypadkach, gdy dane interfejsu do zachowania są proste i lekkie, możesz używać tylko interfejsów API zapisanego stanu instancji, aby zachować dane stanu.

Wykorzystywanie zapisanego stanu za pomocą SavedStateRegistry

Począwszy od Fragmentu 1.1.0 lub jego zależności przechodniej Activity 1.0.0, kontrolery interfejsu, takie jak Activity lub Fragment, implementują SavedStateRegistryOwner i udostępniają SavedStateRegistry powiązany z tym kontrolerem. SavedStateRegistry umożliwia komponentom wykorzystywanie zapisanego stanu kontrolera interfejsu w celu jego używania lub współtworzenia. Na przykład, moduł zapisanego stanu dla ViewModel używa SavedStateRegistry do utworzenia SavedStateHandle i udostępnienia go obiektom ViewModel. Możesz pobrać SavedStateRegistry z poziomu kontrolera interfejsu, wywołując getSavedStateRegistry.

Komponenty, które współtworzą zapisany stan, muszą implementować SavedStateRegistry.SavedStateProvider, który definiuje jedną metodę o nazwie saveState. Metoda saveState() umożliwia komponentowi zwrócenie Bundle zawierającego dowolny stan, który powinien zostać zapisany z tego komponentu. SavedStateRegistry wywołuje tę metodę podczas fazy zapisywania stanu cyklu życia kontrolera interfejsu.

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

Aby zarejestrować SavedStateProvider, wywołaj registerSavedStateProvider() w SavedStateRegistry, przekazując klucz do powiązania z danymi dostawcy oraz dostawcę. Wcześniej zapisane dane dostawcy można pobrać z zapisanego stanu, wywołując consumeRestoredStateForKey() w SavedStateRegistry, przekazując klucz powiązany z danymi dostawcy.

W Activity lub Fragment możesz zarejestrować SavedStateProvider w onCreate() po wywołaniu super.onCreate(). Możesz też ustawić LifecycleObserver w SavedStateRegistryOwner, który implementuje LifecycleOwner, i zarejestrować SavedStateProvider, gdy wystąpi zdarzenie ON_CREATE. Używając LifecycleObserver, możesz oddzielić rejestrację i pobieranie wcześniej zapisanego stanu od samego SavedStateRegistryOwner.

Kotlin

class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val PROVIDER = "search_manager"
        private const val QUERY = "query"
    }

    private val query: String? = null

    init {
        // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
        registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_CREATE) {
                val registry = registryOwner.savedStateRegistry

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this)

                // Get the previously saved state and restore it
                val state = registry.consumeRestoredStateForKey(PROVIDER)

                // Apply the previously saved state
                query = state?.getString(QUERY)
            }
        }
    }

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }

    ...
}

class SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this);

                // Get the previously saved state and restore it
                Bundle state = registry.consumeRestoredStateForKey(PROVIDER);

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

Używanie lokalnego utrwalania danych do obsługi śmierci procesu w przypadku złożonych lub dużych danych

Trwałe lokalne miejsce na dane, takie jak baza danych lub udostępnione preferencje, będzie dostępne tak długo, jak długo aplikacja będzie zainstalowana na urządzeniu użytkownika (chyba że użytkownik wyczyści dane aplikacji). Chociaż takie lokalne miejsce na dane przetrwa śmierć procesu aktywności i aplikacji zainicjowaną przez system, jego pobieranie może być kosztowne, ponieważ będzie trzeba je odczytać z lokalnego miejsca na dane do pamięci. Często to trwałe lokalne miejsce na dane może być już częścią architektury aplikacji, aby przechowywać wszystkie dane, których nie chcesz utracić, gdy otworzysz i zamkniesz aktywność.

Ani ViewModel, ani zapisany stan instancji nie są rozwiązaniami do długotrwałego przechowywania danych, dlatego nie zastępują lokalnego miejsca na dane, takiego jak baza danych. Zamiast tego używaj tych mechanizmów tylko do tymczasowego przechowywania tymczasowego stanu interfejsu, a do innych danych aplikacji używaj pamięci trwałej. Więcej informacji o tym, jak wykorzystać lokalne miejsce na dane do długotrwałego utrwalania danych modelu aplikacji (np. podczas ponownego uruchamiania urządzenia), znajdziesz w przewodniku Architektura aplikacji .

Zarządzanie stanem interfejsu: dziel i rządź

Możesz skutecznie zapisywać i przywracać stan interfejsu, dzieląc pracę między różne typy mechanizmów utrwalania danych. W większości przypadków każdy z tych mechanizmów powinien przechowywać inny typ danych używanych w aktywności, w zależności od kompromisów między złożonością danych, szybkością dostępu i czasem życia:

  • Lokalne utrwalanie danych: przechowuje wszystkie dane aplikacji, których nie chcesz utracić, gdy otworzysz i zamkniesz aktywność.
    • Przykład: kolekcja obiektów piosenek, która może zawierać pliki audio i metadane.
  • ViewModel: przechowuje w pamięci wszystkie dane potrzebne do wyświetlenia powiązanego interfejsu, czyli stan interfejsu ekranu.
    • Przykład: obiekty piosenek z ostatniego wyszukiwania i ostatnie zapytanie.
  • Zapisany stan instancji: przechowuje niewielką ilość danych potrzebnych do ponownego wczytania stanu interfejsu, jeśli system zatrzyma, a potem ponownie utworzy interfejs. Zamiast przechowywać tu złożone obiekty, utrwalaj je w lokalnym miejscu na dane i przechowuj unikalny identyfikator tych obiektów w interfejsach API zapisanego stanu instancji.
    • Przykład: przechowywanie ostatniego zapytania.

Rozważmy na przykład aktywność, która umożliwia wyszukiwanie w bibliotece utworów. Oto jak należy obsługiwać różne zdarzenia:

Gdy użytkownik doda utwór, ViewModel natychmiast deleguje utrwalanie tych danych lokalnie. Jeśli nowo dodany utwór ma być widoczny w interfejsie, musisz też zaktualizować dane w obiekcie ViewModel, aby odzwierciedlić dodanie utworu. Pamiętaj, aby wszystkie wstawienia do bazy danych wykonywać poza wątkiem głównym.

Gdy użytkownik wyszuka utwór, wszystkie złożone dane utworu wczytane z bazy danych powinny zostać natychmiast zapisane w obiekcie ViewModel jako część stanu interfejsu ekranu.

Gdy aktywność przejdzie w tło, a system wywoła interfejsy API zapisanego stanu instancji, zapytanie powinno zostać zapisane w zapisanym stanie instancji na wypadek ponownego utworzenia procesu. Ponieważ informacje te są niezbędne do wczytania danych aplikacji utrwalonych w tym miejscu, zapisz zapytanie w SavedStateHandle ViewModel. To wszystkie informacje potrzebne do wczytania danych i przywrócenia interfejsu do bieżącego stanu.

Przywracanie złożonych stanów: ponowne składanie elementów

Gdy użytkownik ma wrócić do aktywności, istnieją 2 możliwe scenariusze ponownego utworzenia aktywności:

  • Aktywność jest ponownie tworzona po zatrzymaniu przez system. System ma zapytanie zapisane w pakiecie zapisanego stanu instancji, a interfejs powinien przekazać zapytanie do ViewModel, jeśli nie jest używany SavedStateHandle. ViewModel widzi, że nie ma wyników wyszukiwania w pamięci podręcznej, i deleguje wczytywanie wyników wyszukiwania za pomocą podanego zapytania.
  • Aktywność jest ponownie tworzona po zmianie konfiguracji. Ponieważ instancja ViewModel nie została zniszczona, ViewModel ma wszystkie informacje w pamięci podręcznej i nie musi ponownie wysyłać zapytań do bazy danych.

Dodatkowe materiały

Więcej informacji o zapisywaniu stanów interfejsu znajdziesz w tych materiałach.

Blogi

Codelabs