Zapisywanie stanów interfejsu

W tym przewodniku omawiamy oczekiwania użytkowników dotyczące stanu UI i dostępne opcje jego zachowania.

Zapisywanie i przywracanie stanu interfejsu użytkownika natychmiast po zniszczeniu przez system działań lub aplikacji jest niezbędne dla wygody użytkowników. Użytkownicy oczekują, że stan interfejsu użytkownika pozostanie taki sam, ale system może zniszczyć aktywność i jej zapisany stan.

Aby połączyć oczekiwania użytkowników i zachowania systemu, połącz te metody:

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

Dopilnuj, aby aplikacja spełniała oczekiwania użytkowników i zapewniała szybki, responsywny interfejs. Unikaj opóźnień w ładowaniu danych do interfejsu, szczególnie w przypadku typowych zmian konfiguracji, takich jak rotacja.

Oczekiwania użytkowników i zachowanie systemu

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

Odrzucenie stanu interfejsu inicjowanego przez użytkownika

Użytkownik oczekuje, że po rozpoczęciu działania jego stan w interfejsie będzie taki sam, dopóki nie zamknie go całkowicie. Użytkownik może całkowicie odrzucić aktywność, wykonując te czynności:

  • Usuwanie aktywności z ekranu Przegląd (ostatnie).
  • Zatrzymanie aplikacji lub wymuszenie jej zamknięcia na ekranie Ustawienia.
  • Uruchamiam ponownie urządzenie.
  • wykonanie jakiegoś działania „zakończenia” (obsługiwanego przez funkcję Activity.finish()).

W tych całkowitych przypadkach odrzucenia użytkownik zakłada, że trwale zrezygnował z aktywności, a jeśli ponownie ją uruchomił, oczekuje, że zacznie się ona od nienaruszonego stanu. Podstawowe działanie systemu w tych scenariuszach odrzucenia jest zgodne z oczekiwaniami użytkowników – instancja aktywności zostanie zniszczona i usunięta z pamięci wraz z zapisanym w niej stanem i z zapisanymi rekordami stanu instancji powiązanymi z daną aktywnością.

Istnieją pewne wyjątki od tej reguły obejmującej pełne zamknięcie, np. użytkownik może oczekiwać, że przeglądarka przed zamknięciem przeglądarki za pomocą przycisku Wstecz przeniesie go dokładnie na tę stronę, którą przeglądał.

Odrzucenie stanu interfejsu inicjowanego przez system

Użytkownik oczekuje, że stan interfejsu aktywności pozostanie taki sam po zmianie konfiguracji, np. podczas rotacji lub przełączenia się w tryb wielu okien. Domyślnie jednak system niszczy aktywność po wystąpieniu takiej zmiany konfiguracji, usuwając wszystkie stany interfejsu użytkownika zapisane w instancji aktywności. Więcej informacji o konfiguracjach urządzeń znajdziesz na stronie z informacjami o konfiguracjach. Pamiętaj, że możliwe jest (choć nie jest to zalecane) zastąpienie domyślnego działania w przypadku zmian konfiguracji. Więcej informacji znajdziesz w sekcji Samodzielne wprowadzanie zmian w 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 do niej wróci. Na przykład użytkownik przeprowadza wyszukiwanie w Twojej aktywności związanej z wyszukiwaniem, a następnie klika przycisk ekranu głównego lub odbiera telefon. Po powrocie do aktywności związanej z wyszukiwaniem spodziewa się, że znajdzie tam słowo kluczowe wraz z wynikami – dokładnie tak samo jak wcześniej.

W takiej sytuacji aplikacja jest umieszczona w tle, a system robi wszystko, co może, by jej proces został zapisany w pamięci. System może jednak zniszczyć proces aplikacji, gdy użytkownika nie ma w interakcji z innymi aplikacjami. W takim przypadku instancja aktywności jest niszczona wraz z każdym zapisanym w niej stanem. Gdy użytkownik ponownie uruchomi aplikację, aktywność zostanie nieoczekiwanie oczyszczona. Więcej informacji o śmierci procesów znajdziesz w artykule Procesy i cykl życia aplikacji.

Opcje zachowywania stanu interfejsu użytkownika

Gdy oczekiwania użytkownika dotyczące stanu interfejsu użytkownika nie odpowiadają domyślnemu działaniu systemu, musisz zapisać i przywrócić stan interfejsu użytkownika, aby zapewnić, że zniszczenie inicjowane przez system nie będzie widoczne dla użytkownika.

Każda z opcji zachowania stanu interfejsu użytkownika różni się pod względem tych wymiarów, które wpływają na wygodę użytkowników:

ViewModel Zapisany stan instancji Pamięć trwała
Lokalizacja pamięci w pamięci w pamięci na dysku lub sieci
Przetrwa zmianę konfiguracji Tak Tak Tak
Przetrwa śmierć procesu uruchomionego przez system Nie Tak Tak
Przetrwa odrzucenie zakończonej aktywności użytkownika/onFinish() Nie Nie Tak
Ograniczenia danych złożone obiekty są prawidłowe, ale przestrzeń jest ograniczona przez dostępną pamięć tylko w przypadku typów podstawowych i prostych, małych obiektów, takich jak String (ciąg znaków). ograniczona tylko przez miejsce na dysku lub koszt / czas pobierania z zasobu sieciowego;
Czas odczytu/zapisu szybki (tylko dostęp do pamięci) powolne (wymaga serializacji/deserializacji) wolne (wymaga dostępu do dysku lub transakcji sieciowej)

Używanie obiektu ViewModel do obsługi zmian konfiguracji

Model ViewModel jest idealny do przechowywania danych związanych z interfejsem użytkownika i zarządzania nimi, gdy użytkownik aktywnie korzysta z aplikacji. Zapewnia szybki dostęp do danych interfejsu i pomaga uniknąć ponownego pobierania danych z sieci lub dysku podczas rotacji, zmiany rozmiaru okna i innych często występujących zmian konfiguracji. Aby dowiedzieć się, jak wdrożyć model ViewModel, zapoznaj się z przewodnikiem po modelu ViewModel.

ViewModel zachowuje dane w pamięci, co oznacza, że ich pobranie jest tańsze niż dane z dysku lub sieci. Model ViewModel jest powiązany z aktywnością (lub z innym właścicielem cyklu życia) – pozostaje w pamięci podczas zmiany konfiguracji, a system automatycznie wiąże go z nowym wystąpieniem aktywności, który wynika ze zmiany konfiguracji.

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

W przeciwieństwie do zapisanego stanu instancji modele ViewModel są niszczone podczas śmierci procesu inicjowanego przez system. Aby ponownie wczytać dane po zakończeniu procesu zainicjowanego przez system w ViewModel, użyj SavedStateHandle API. Jeśli dane są powiązane z interfejsem użytkownika i nie muszą być przechowywane w modelu ViewModel, użyj funkcji onSaveInstanceState() w systemie View lub rememberSaveable w Jetpack Compose. Jeśli dane to dane aplikacji, lepiej zachować je na dysku.

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

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

Wywołanie zwrotne onSaveInstanceState() w systemie Widok, rememberSaveable w Jetpack Compose i SavedStateHandle w modelach ViewModels przechowują dane potrzebne do ponownego załadowania stanu kontrolera interfejsu użytkownika, np. działania lub fragmentu, jeśli system zniszczy, a następnie odtworzy ten kontroler. Aby dowiedzieć się, jak wdrożyć zapisany stan instancji za pomocą onSaveInstanceState, zapoznaj się z sekcją Zapisywanie i przywracanie stanu aktywności w przewodniku po cyklu życia aktywności.

Zapisane pakiety stanu instancji są zachowywane zarówno w przypadku zmian konfiguracji, jak i śmierci procesu, ale są ograniczone przez ilość miejsca i szybkość, ponieważ różne interfejsy API serialują dane. Serializacja może zużywać dużo pamięci, jeśli obiekty są złożone. Ponieważ ten proces ma miejsce w wątku głównym podczas zmiany konfiguracji, długotrwała serializacja może powodować porzucanie klatek i zacinanie się wizualne.

Nie używaj zapisanego stanu instancji do przechowywania dużych ilości danych, takich jak mapy bitowe ani złożone struktury danych, które wymagają długiej serializacji lub deerializacji. Zamiast tego przechowuj tylko typy podstawowe i proste, małe obiekty, takie jak String. Dlatego używaj zapisanego stanu instancji, aby przechowywać minimalną ilość danych, takich jak identyfikator, niezbędną do odtworzenia danych niezbędnych do przywrócenia interfejsu użytkownika do poprzedniego stanu, jeśli inne mechanizmy trwałości przestaną działać. Większość aplikacji powinna wdrożyć tę funkcję, aby obsługiwać śmierć procesu zainicjowanego przez system.

W zależności od przypadków użycia aplikacji użycie zapisanego stanu instancji może być niepotrzebne. Na przykład przeglądarka może przenieść użytkownika z powrotem na tę stronę, którą przeglądał przed zamknięciem. Jeśli Twoja aktywność działa w ten sposób, możesz zrezygnować z używania zapisanego stanu instancji i zachować wszystko lokalnie.

Dodatkowo, gdy otworzysz działanie z intencji, do działania jest dostarczany pakiet dodatków, zarówno po zmianie konfiguracji, jak i po przywróceniu aktywności przez system. Jeśli przy uruchomieniu działania jakaś informacja o stanie interfejsu, np. zapytanie wyszukiwania, została przekazana jako intencja dodatkowa, możesz użyć pakietu dodatków zamiast zapisanego pakietu stanu instancji. Więcej informacji o dodatkowych intencjach znajdziesz w artykule Filtry intencji i intencji.

W każdym z tych scenariuszy nadal używaj ViewModel, aby uniknąć marnowania cykli ładowania danych z bazy danych podczas zmiany konfiguracji.

Jeśli dane interfejsu użytkownika do zachowania są proste i nieskomplikowane, do zachowania danych stanu możesz używać tylko interfejsów API stanu instancji.

Uzyskiwanie dostępu do zapisanego stanu za pomocą SavedStateRegistry

Począwszy od fragmentu 1.1.0 lub jego aktywności 1.0.0 pośredniej zależności, kontrolery UI, takie jak Activity czy Fragment, implementują SavedStateRegistryOwner i udostępniają kod SavedStateRegistry powiązany z tym kontrolerem. SavedStateRegistry umożliwia komponentom połączenie się z zapisanym stanem kontrolera UI w celu jego wykorzystania lub współtworzenia. Na przykład moduł zapisanego stanu dla obiektu ViewModel używa metody SavedStateRegistry do tworzenia obiektu SavedStateHandle i udostępniania go obiektom ViewModel. Możesz pobrać obiekt SavedStateRegistry z kontrolera UI, wywołując metodę getSavedStateRegistry().

Komponenty, które wpływają na zapisany stan, muszą implementować metodę SavedStateRegistry.SavedStateProvider, która określa jedną metodę o nazwie saveState(). Metoda saveState() umożliwia komponentowi zwracanie wartości Bundle zawierającej dowolny stan, który powinien zostać zapisany z tego komponentu. SavedStateRegistry wywołuje tę metodę na etapie zapisywania stanu cyklu życia kontrolera interfejsu użytkownika.

Kotlin

class SearchManager : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val QUERY = "query"
    }

    private val query: String? = null

    ...

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

Java

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ć obiekt SavedStateProvider, wywołaj registerSavedStateProvider() w SavedStateRegistry, przekazując klucz do powiązania z danymi dostawcy oraz z dostawcą. Dane zapisane wcześniej dla dostawcy można pobrać z zapisanego stanu, wywołując metodę consumeRestoredStateForKey() w usłudze SavedStateRegistry, przekazując klucz powiązany z danymi dostawcy.

W ciągu Activity lub Fragment możesz zarejestrować SavedStateProvider w onCreate() po wywołaniu usługi super.onCreate(). Możesz też skonfigurować LifecycleObserver w SavedStateRegistryOwner, który implementuje LifecycleOwner, i zarejestrować SavedStateProvider po wystąpieniu zdarzenia ON_CREATE. Używając żądania LifecycleObserver, możesz odłączyć rejestrację i pobieranie wcześniej zapisanego stanu od samego parametru 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żywaj lokalnej trwałości w przypadku śmierci procesów w przypadku złożonych lub dużych danych

Stała pamięć lokalna, np. baza danych lub wspólne ustawienia, będzie obowiązywać tak długo, jak długo aplikacja będzie zainstalowana na urządzeniu użytkownika (chyba że użytkownik wyczyści dane aplikacji). Taka pamięć lokalna przetrwa aktywność inicjowaną przez system i śmierć procesu aplikacji, ale jej odzyskanie może być kosztowne, ponieważ musi zostać odczytany z pamięci lokalnej w pamięci lokalnej. Często ta stała pamięć lokalna może już być częścią architektury Twojej aplikacji i przechowywać wszystkie dane, których nie chcesz utracić po otwarciu i zamknięciu działania.

Ani ViewModel, ani zapisany stan instancji nie są rozwiązaniami do długoterminowej pamięci masowej i nie zastępują lokalnej pamięci, takiej jak baza danych. Zamiast tego używaj tych mechanizmów do tymczasowego przechowywania danych o stanie interfejsu i używania pamięci trwałej na inne dane aplikacji. Więcej informacji o tym, jak korzystać z pamięci lokalnej, by zachować dane modelu aplikacji w dłuższym okresie (np. przy ponownym uruchomieniu urządzenia), znajdziesz w przewodniku po architekturze aplikacji.

Zarządzanie stanem interfejsu użytkownika: dzielenie i osiąganie

Możesz wydajnie zapisywać i przywracać stan interfejsu, dzieląc pracę między różne typy mechanizmów trwałych. W większości przypadków każdy z tych mechanizmów powinien przechowywać inny typ danych używanych w działaniu, uwzględniając w zależności od złożoności danych, szybkości dostępu i czasu trwania:

  • Trwałość lokalna: przechowuje wszystkie dane aplikacji, których nie chcesz utracić w przypadku otwarcia i zamknięcia działania.
    • Przykład: zbiór obiektów utworów, który może zawierać pliki audio i metadane.
  • ViewModel: zapisuje w pamięci wszystkie dane potrzebne do wyświetlenia powiązanego interfejsu, czyli stanu interfejsu ekranu.
    • Przykład: obiekty utworu związane z ostatnim wyszukiwaniem i ostatnim wyszukiwanym hasłem.
  • Zapisany stan instancji: zapisuje niewielką ilość danych potrzebnych do ponownego załadowania stanu interfejsu, jeśli system zatrzyma i odtworzy interfejs. Zamiast przechowywać tutaj złożone obiekty, zachowuj je w pamięci lokalnej i przechowuj w pamięciach lokalnych unikalny identyfikator tych obiektów w interfejsach API zapisanych stanów instancji.
    • Przykład: przechowywanie ostatniego zapytania.

Weźmy za przykład działanie, które umożliwia przeszukiwanie biblioteki utworów. Oto jak należy postępować z różnymi zdarzeniami:

Gdy użytkownik doda utwór, ViewModel natychmiast przekaże te dane lokalnie. Jeśli ten nowo dodany utwór powinien być widoczny w interfejsie, musisz też zaktualizować dane w obiekcie ViewModel, aby odzwierciedlić dodanie utworu. Pamiętaj, aby zrobić wszystkie wstawienia bazy danych z wątku głównego.

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

Gdy aktywność jest wykonywana w tle, a system wywołuje interfejsy API zapisanych stanów instancji, wyszukiwane zapytanie powinno zostać zapisane w zapisanym stanie instancji na wypadek odtworzenia procesu. Informacje są niezbędne do wczytania danych aplikacji, które zostały w nim zapisane, dlatego zapisz zapytanie w modelu ViewModel SavedStateHandle. To wszystkie informacje potrzebne do wczytania danych i przywrócenia interfejsu do aktualnego stanu.

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

Gdy nadejdzie pora, by użytkownik wrócił do aktywności, możliwe są 2 sytuacje jej odtwarzania:

  • Aktywność jest ponownie uruchamiana po zatrzymaniu przez system. System ma zapytanie zapisane w zapisanym pakiecie stanu instancji, a interfejs powinien przekazać je do interfejsu ViewModel, jeśli nie jest używana metoda SavedStateHandle. ViewModel widzi, że nie ma żadnych wyników wyszukiwania w pamięci podręcznej, i przekazuje do wczytywania wyniki wyszukiwania za pomocą danego zapytania.
  • Aktywność jest tworzona po zmianie konfiguracji. Instancja ViewModel nie została zniszczona, dlatego ViewModel przechowuje 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 użytkownika znajdziesz w poniższych materiałach.

Blogi