Zapisywanie stanów interfejsu

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

Szybkie zapisywanie i przywracanie stanu interfejsu po zniszczeniu przez system aktywności hosta lub procesu aplikacji jest niezbędne, aby zapewnić użytkownikom wygodę. Użytkownicy oczekują, że stan interfejsu pozostanie taki sam, ale system może zniszczyć Activity hostującą ekran i jej zapisany stan.

Aby zniwelować różnicę między oczekiwaniami użytkowników a działaniem systemu, zastosuj kombinację tych metod:

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

Zadbaj o to, aby aplikacja spełniała oczekiwania użytkowników i oferowała szybki, responsywny interfejs. Unikaj opóźnień podczas wczytywania danych w interfejsie, zwłaszcza po wprowadzeniu typowych zmian w konfiguracji, takich jak rotacja.

Oczekiwania użytkowników i zachowanie systemu

W zależności od podjętego działania użytkownik oczekuje, że stan interfejsu zostanie wyczyszczony lub zachowany. W niektórych przypadkach system automatycznie wykonuje to, czego oczekuje użytkownik. W innych przypadkach system robi odwrotnie.

Zamykanie stanu interfejsu zainicjowane przez użytkownika

Użytkownik oczekuje, że gdy przejdzie do ekranu, jego tymczasowy stan interfejsu pozostanie taki sam, dopóki go całkowicie nie zamknie. Użytkownik może całkowicie zamknąć ekran lub aplikację, wykonując te czynności:

  • przesunięcie aplikacji z ekranu Przegląd (Ostatnie);
  • Zamykanie lub wymuszanie zamknięcia aplikacji na ekranie Ustawienia.
  • Uruchom ponownie urządzenie.
  • wykonanie jakiejś czynności „końcowej” (która jest obsługiwana przez Activity.finish());

W takich przypadkach użytkownik zakłada, że na stałe opuścił ekran, i oczekuje, że po powrocie ekran będzie w stanie początkowym. Działanie systemu w tych scenariuszach zamykania jest zgodne z oczekiwaniami użytkownika – instancja aktywności hosta zostanie zniszczona i usunięta z pamięci wraz z zapisanym w niej stanem i powiązanym z nią zapisanym rekordem stanu.

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

Zamykanie stanu interfejsu zainicjowane przez system

Użytkownik oczekuje, że stan interfejsu ekranu pozostanie taki sam podczas zmiany konfiguracji, np. obrócenia ekranu lub przejścia do trybu wielu okien. Domyślnie jednak system niszczy aktywność hosta, gdy nastąpi taka zmiana konfiguracji, usuwając wszelkie zapisane w niej stany interfejsu. Więcej informacji o konfiguracjach urządzeń znajdziesz w artykule Reagowanie na zmiany konfiguracji w Jetpack Compose.

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

Użytkownik oczekuje też, że stan interfejsu aplikacji pozostanie taki sam, jeśli tymczasowo przełączy się na inną aplikację, a potem wróci do Twojej aplikacji. Na przykład użytkownik przeprowadza wyszukiwanie na ekranie, a potem naciska przycisk ekranu głównego lub odbiera połączenie telefoniczne. Gdy wróci do ekranu wyszukiwania, oczekuje, że słowo kluczowe i wyniki wyszukiwania będą nadal widoczne, dokładnie tak jak wcześniej.

W takim przypadku 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 nie korzysta z niej i używa innych aplikacji. W takim przypadku aktywność hosta zostanie zniszczona wraz z wszelkimi zapisanymi w niej stanami. Gdy użytkownik ponownie uruchomi aplikację, ekran będzie w nieoczekiwanym stanie. Więcej informacji o śmierci procesu znajdziesz w artykule Procesy i cykl życia aplikacji.

Opcje zachowywania 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, aby zniszczenie zainicjowane przez system było dla użytkownika niezauważalne.

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

ViewModel Stan zapisany Pamięć trwała
Lokalizacja zapisu w pamięci w pamięci na dysku lub w sieci.
Przetrwanie zmiany konfiguracji Tak Tak Tak
Przetrwanie śmierci procesu zainicjowanej przez system Nie Tak Tak
Wytrzymuje całkowite zamknięcie ekranu przez użytkownika finish() Nie Nie Tak
Ograniczenia dotyczące danych złożone obiekty są w porządku, ale przestrzeń jest ograniczona dostępną pamięcią; tylko w przypadku typów podstawowych i prostych, małych obiektów, takich jak String ograniczone jedynie miejscem na dysku lub kosztem / czasem pobierania z zasobu sieciowego.
Czas odczytu/zapisu szybkie (tylko dostęp do pamięci), powolne (wymaga serializacji/deserializacji); powolne (wymaga dostępu do dysku lub transakcji sieciowej);

Używanie klasy ViewModel do obsługi zmian konfiguracji

ViewModel idealnie nadaje się do przechowywania danych związanych z interfejsem i zarządzania nimi, 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 obracania ekranu, zmiany rozmiaru okna i innych często występujących zmian konfiguracji. Aby dowiedzieć się, jak wdrożyć ViewModel, zapoznaj się z przewodnikiem po ViewModelu.

ViewModel przechowuje dane w pamięci, co oznacza, że ich pobieranie jest tańsze niż pobieranie danych z dysku lub sieci. Obiekt ViewModel jest powiązany z właścicielem cyklu życia, takim jak miejsce docelowe nawigacji lub aktywność. Pozostaje w pamięci podczas zmiany konfiguracji, a system automatycznie kojarzy ViewModel z nową instancją właściciela cyklu życia, która powstaje w wyniku zmiany konfiguracji.

W przeciwieństwie do stanu zapisanego obiekty ViewModel są niszczone podczas śmierci procesu inicjowanej przez system. Aby ponownie wczytać dane po śmierci procesu zainicjowanego przez system w obiekcie ViewModel, użyj interfejsu SavedStateHandle API. Jeśli dane są powiązane z interfejsem i nie muszą być przechowywane w obiekcie ViewModel, użyj rememberSerializable. W przypadku podstawowych typów danych lub sytuacji, w których nie chcesz używać @Serializable, użyj rememberSaveable. Jeśli dane są danymi aplikacji, lepiej zapisać je na dysku.

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

Używanie zapisanego stanu jako kopii zapasowej w przypadku śmierci procesu zainicjowanej przez system

Interfejsy API, takie jak rememberSerializablerememberSaveable w Compose oraz SavedStateHandle w ViewModelach, przechowują dane potrzebne do ponownego wczytania stanu interfejsu, jeśli system zniszczy, a potem ponownie utworzy komponent. Aby wydajniej obsługiwać złożone struktury danych, SavedStateHandle obsługuje Kotlinx Serialization za pomocą rozszerzenia saved {}, co umożliwia bezproblemowe zapisywanie i przywracanie obiektów bezpiecznych pod względem typów wraz ze standardowymi typami prostymi. Aby dowiedzieć się, jak zaimplementować zapisany stan za pomocą rememberSaveable, zapoznaj się z artykułem Stan i Jetpack Compose.

Zapisane pakiety stanu 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 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 podstawowe i proste, małe obiekty, takie jak String. Dlatego stan zapisany służy do przechowywania minimalnej ilości niezbędnych danych, takich jak identyfikator, aby w razie awarii innych mechanizmów trwałości odtworzyć dane potrzebne do przywrócenia interfejsu do poprzedniego stanu. Większość aplikacji powinna to implementować, aby obsługiwać śmierć procesu zainicjowaną przez system.

W zależności od przypadków użycia aplikacji może nie być konieczne używanie stanu zapisanego. Na przykład przeglądarka może przywrócić dokładnie tę stronę, którą użytkownik przeglądał przed zamknięciem przeglądarki. Jeśli Twoja aktywność zachowuje się w ten sposób, możesz zrezygnować z używania zapisanego stanu i zamiast tego przechowywać wszystko lokalnie.

Dodatkowo, gdy otworzysz aktywność z intencji, pakiet dodatków jest dostarczany do aktywności zarówno wtedy, gdy zmienia się konfiguracja, jak i wtedy, gdy system przywraca aktywność. Jeśli fragment danych stanu interfejsu, np. zapytanie wyszukiwania, został przekazany jako dodatkowy element intencji podczas uruchamiania aktywności, możesz użyć pakietu dodatkowych elementów zamiast pakietu stanu zapisanego. Więcej informacji o dodatkach do intencji znajdziesz w artykule Intencje i filtry intencji.

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

Jeśli dane interfejsu, które chcesz zachować, są proste i niewielkie, możesz użyć samych interfejsów API stanu zapisanego, aby zachować dane stanu.

Korzystanie z zapisanego stanu za pomocą interfejsu SavedStateRegistry

Od Fragmentu 1.1.0 lub jego zależności przechodniej Activity 1.0.0 komponenty interfejsu, takie jak ComponentActivity, implementują SavedStateRegistryOwner i udostępniają SavedStateRegistry, który jest powiązany z tym komponentem. SavedStateRegistry umożliwia komponentom podłączenie się do zapisanego stanu w celu korzystania z niego lub jego modyfikowania. Na przykład moduł Saved State dla ViewModel używa SavedStateRegistry do tworzenia SavedStateHandle i udostępniania go obiektom ViewModel. Możesz pobrać SavedStateRegistry z właściciela cyklu życia, wywołując savedStateRegistry.

Komponenty, które przyczyniają się do zapisywania stanu, muszą implementować interfejs SavedStateRegistry.SavedStateProvider, który definiuje jedną metodę o nazwie saveState(). Metoda saveState() umożliwia komponentowi zwrócenie obiektu Bundle zawierającego dowolny stan, który powinien zostać zapisany z tego komponentu. SavedStateRegistry wywołuje tę metodę w fazie zapisywania cyklu życia właściciela cyklu życia.

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

Aby zarejestrować SavedStateProvider, zadzwoń pod numer registerSavedStateProvider() na SavedStateRegistry, przekazując klucz do powiązania z danymi dostawcy, a także dostawcę. Wcześniej zapisane dane dostawcy można pobrać ze stanu zapisanego, wywołując consumeRestoredStateForKey() na SavedStateRegistry i przekazując klucz powiązany z danymi dostawcy.

W ramach ComponentActivity możesz zarejestrować SavedStateProvideronCreate() po wywołaniu super.onCreate(). Możesz też ustawić LifecycleObserver na SavedStateRegistryOwner, który implementuje LifecycleOwner, i zarejestrować SavedStateProvider po wystąpieniu zdarzenia ON_CREATE. Używając LifecycleObserver, możesz oddzielić rejestrację i pobieranie wcześniej zapisanego stanu od samego SavedStateRegistryOwner.

  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 SearchActivity : ComponentActivity() {
    private var searchManager = SearchManager(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set up your Compose UI here
        setContent {
            // ...
        }
    }
  }

Używaj 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 DataStore, 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ż taka pamięć lokalna przetrwa śmierć procesu aplikacji zainicjowaną przez system, jej pobranie może być kosztowne, ponieważ będzie musiała zostać odczytana z pamięci lokalnej do pamięci. Często ta trwała pamięć lokalna może być już częścią architektury aplikacji, w której przechowujesz wszystkie dane, których nie chcesz utracić po otwarciu i zamknięciu aplikacji.

Ani ViewModel, ani stan zapisany za pomocą rememberSerializable, rememberSaveable lub SavedStateHandle nie są rozwiązaniami do długotrwałego przechowywania danych, a tym samym nie zastępują pamięci lokalnej, takiej jak baza danych. Zamiast tego używaj tych mechanizmów tylko do tymczasowego przechowywania stanu interfejsu, a do przechowywania innych danych aplikacji używaj pamięci trwałej. Więcej informacji o tym, jak wykorzystać pamięć lokalną do długotrwałego przechowywania danych modelu aplikacji (np. po ponownym uruchomieniu urządzenia), znajdziesz w Przewodniku po architekturze 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 trwałości. W większości przypadków każdy z tych mechanizmów powinien przechowywać inny typ danych używanych w aplikacji, w zależności od kompromisów między złożonością danych, szybkością dostępu i czasem życia:

  • Lokalne przechowywanie danych: przechowuje wszystkie dane aplikacji, których nie chcesz utracić, gdy otworzysz i zamkniesz aplikację.
    • Przykład: zbiór obiektów utworów, który 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 utworów z ostatniego wyszukiwania i ostatnie zapytanie.
  • Stan zapisany (rememberSerializable, rememberSaveableSavedStateHandle): przechowuje niewielką ilość danych potrzebnych do ponownego wczytania stanu interfejsu, jeśli system zatrzyma się, a następnie ponownie utworzy interfejs. Zamiast przechowywać tutaj złożone obiekty, zapisuj je w pamięci lokalnej, a w interfejsach API stanu zapisanego przechowuj unikalny identyfikator tych obiektów.
    • Przykład: przechowywanie ostatniego zapytania.

Rozważmy na przykład aplikację, która umożliwia wyszukiwanie w bibliotece utworów. Oto jak należy postępować w przypadku różnych zdarzeń:

Gdy użytkownik doda utwór, ViewModel natychmiast deleguje zapisywanie tych danych lokalnie. Jeśli nowo dodana piosenka ma być wyświetlana w interfejsie, musisz też zaktualizować dane w obiekcie ViewModel, aby odzwierciedlały dodanie piosenki. Pamiętaj, aby wszystkie wstawienia do bazy danych wykonywać poza wątkiem głównym.

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

Gdy aplikacja przechodzi w tło i system zapisuje jej stan, zapytanie powinno być przechowywane za pomocą interfejsów API zapisanego stanu na wypadek ponownego utworzenia procesu. Ponieważ te informacje są niezbędne do wczytania danych aplikacji zapisanych w tym miejscu, przechowuj zapytanie w obiekcie ViewModelSavedStateHandle lub używaj adnotacji rememberSerializable lub rememberSaveable w funkcjach kompozycyjnych. 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 wraca do aplikacji, istnieją 2 możliwe scenariusze ponownego tworzenia interfejsu:

  • Interfejs użytkownika zostanie odtworzony po zakończeniu procesu aplikacji przez system. System ma zapisane zapytanie za pomocą interfejsów API stanu zapisanego. Funkcja ViewModel (używająca SavedStateHandle) lub funkcja kompozycyjna (używająca rememberSerializable lub rememberSaveable) automatycznie przywraca zapytanie. Jeśli funkcja kompozycyjna przywróci zapytanie, przekaże je do ViewModel. ViewModel widzi, że nie ma w pamięci podręcznej wyników wyszukiwania, i przekazuje zadanie wczytania wyników wyszukiwania za pomocą podanego zapytania.
  • Po zmianie konfiguracji interfejs użytkownika jest tworzony od nowa. Ponieważ instancja ViewModel nie została zniszczona, ViewModel ma wszystkie informacje zapisane 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.

Codelabs

Wyświetlanie treści