Pojęcia i implementacja w Jetpack Compose
W tym przewodniku omawiamy oczekiwania użytkowników dotyczące stanu interfejsu i dostępne opcje zachowywania stanu.
Szybkie zapisywanie i przywracanie stanu interfejsu aktywności po zniszczeniu aktywności lub aplikacji przez system jest niezbędne, aby zapewnić użytkownikom dobre wrażenia. 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 działaniem systemu, zastosuj kombinację tych metod:
ViewModelobiektów.- Zapisane stany instancji w tych kontekstach:
- Wyświetlenia:
onSaveInstanceState()API. - Modele widoku:
SavedStateHandle.
- Wyświetlenia:
- Pamięć lokalna do przechowywania stanu interfejsu podczas przechodzenia między aplikacjami i aktywnościami.
Optymalne rozwiązanie zależy od złożoności danych interfejsu, przypadków użycia aplikacji i 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 do interfejsu, zwłaszcza po wprowadzeniu typowych zmian w konfiguracji, takich jak rotacja.
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 wykonuje czynności, których oczekuje użytkownik. W innych przypadkach system robi coś przeciwnego.
Zamykanie stanu interfejsu zainicjowane przez użytkownika
Użytkownik oczekuje, że po rozpoczęciu działania tymczasowy stan interfejsu tego działania pozostanie taki sam, dopóki użytkownik całkowicie go nie zamknie. Użytkownik może całkowicie odrzucić aktywność, wykonując te czynności:
- przesunięcie aktywności z ekranu przeglądu (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 trwale opuścił działanie, i oczekuje, że po ponownym otwarciu działanie rozpocznie się od początku. Działanie systemu w tych scenariuszach zamykania jest zgodne z oczekiwaniami użytkownika – instancja aktywności zostanie zniszczona i usunięta z pamięci wraz z zapisanym w niej stanem i rekordem zapisanego stanu instancji powiązanym z aktywnością.
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 aktywności pozostanie taki sam podczas zmiany konfiguracji, np. obrócenia ekranu lub przejścia do trybu wielu okien. Domyślnie jednak system niszczy aktywność, gdy nastąpi taka zmiana konfiguracji, usuwając wszelkie stany interfejsu przechowywane w instancji aktywności. Więcej informacji o konfiguracjach urządzeń znajdziesz na stronie z dokumentacją konfigurowania.
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 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 aplikacji do wyszukiwania, a potem naciska przycisk ekranu głównego lub odbiera połączenie. Gdy wraca do aplikacji do wyszukiwania, oczekuje, że słowo kluczowe w sieci wyszukiwania 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 instancja aktywności jest niszczona wraz z przechowywanym w niej stanem. Gdy użytkownik ponownie uruchomi aplikację, aktywność jest w nieoczekiwanym stanie czystym. Więcej informacji o śmierci procesu znajdziesz w artykule Procesy i cykl życia aplikacji.
Opcje zachowywania stanu interfejsu
Jeśli oczekiwania użytkownika dotyczące stanu interfejsu nie są zgodne z domyślnym działaniem 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 |
Zapisany stan instancji |
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 |
Przetrwanie po odrzuceniu/zakończeniu aktywności przez użytkownika |
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 |
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 aktywnością (lub innym właścicielem cyklu życia) – pozostaje w pamięci podczas zmiany konfiguracji, a system automatycznie wiąże go z nową instancją aktywności, która powstaje w wyniku zmiany konfiguracji.
Obiekty ViewModel są automatycznie niszczone przez system, gdy użytkownik wraca z aktywności lub fragmentu albo gdy wywołasz metodę finish(). Oznacza to, że stan jest czyszczony zgodnie z oczekiwaniami użytkownika w tych scenariuszach.
W przeciwieństwie do zapisanego stanu instancji obiekty ViewModel są niszczone podczas śmierci procesu zainicjowanej przez system. Aby ponownie wczytać dane po śmierci procesu zainicjowanego przez system w obiekcie ViewModel, użyj SavedStateHandle interfejsu API. Jeśli dane są powiązane z interfejsem i nie muszą być przechowywane w obiekcie ViewModel, użyj onSaveInstanceState(). Jeśli dane to dane 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 instancji jako kopii zapasowej w przypadku śmierci procesu zainicjowanej przez system
Wywołanie zwrotne onSaveInstanceState() w systemie View i SavedStateHandle w ViewModelach przechowuje dane potrzebne do ponownego wczytania stanu kontrolera interfejsu, np. działania lub fragmentu, jeśli system zniszczy i później ponownie utworzy ten kontroler. Aby dowiedzieć się, jak zaimplementować zapisany stan instancji za pomocą onSaveInstanceState, zapoznaj się z sekcją Zapisywanie i przywracanie stanu działania w przewodniku po cyklu życia działania.
Zapisane pakiety stanu instancji są zachowywane zarówno podczas zmian konfiguracji, jak i w przypadku ś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 podstawowe i proste, małe obiekty, takie jak String. Dlatego używaj zapisanego stanu instancji 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 się okazać, że nie musisz w ogóle korzystać z zapisanego stanu instancji. 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 instancji i zamiast tego przechowywać wszystko lokalnie.
Dodatkowo, gdy otworzysz aktywność z intencji, pakiet dodatków zostanie dostarczony do aktywności zarówno wtedy, gdy zmieni się konfiguracja, jak i wtedy, gdy system przywróci aktywność.
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 wystąpienia, aby zachować dane stanu.
Korzystanie z zapisanego stanu za pomocą interfejsu 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ą interfejs SavedStateRegistryOwner i udostępniają SavedStateRegistry powiązany z tym kontrolerem. SavedStateRegistry umożliwia komponentom podłączenie się do zapisanego stanu kontrolera interfejsu, aby go wykorzystywać lub do niego wnosić. 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 kontrolera interfejsu, wywołując getSavedStateRegistry.
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 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, 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 Activity lub Fragment możesz zarejestrować SavedStateProvider w onCreate() 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.
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 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 ustawienia udostępnione, 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 działania zainicjowane przez system i śmierć procesu aplikacji, pobieranie z niego danych może być kosztowne, ponieważ trzeba je będzie odczytać z pamięci lokalnej do pamięci. Często ta trwała pamięć lokalna jest już częścią architektury aplikacji, w której przechowujesz wszystkie dane, których nie chcesz utracić po otwarciu i zamknięciu aktywności.
Ani ViewModel, ani zapisany stan instancji nie są rozwiązaniami do długoterminowego przechowywania danych, dlatego 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 aktywności, 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 aktywność.
- 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.
- Zapisany stan instancji: przechowuje niewielką ilość danych potrzebnych do ponownego wczytania stanu interfejsu, jeśli system zatrzyma się, a następnie ponownie utworzy interfejs. Zamiast przechowywać tu złożone obiekty, zapisuj je w pamięci lokalnej, a w interfejsach API stanu zapisanej instancji przechowuj unikalny identyfikator tych obiektów.
- Przykład: przechowywanie ostatniego zapytania.
Rozważmy na przykład aktywność, 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 aktywność przechodzi w tło i system wywołuje interfejsy API stanu zapisanej instancji, zapytanie powinno być przechowywane w stanie zapisanej instancji na wypadek, gdyby proces został ponownie utworzony. Ponieważ te informacje są niezbędne do wczytania danych aplikacji zapisanych w tym miejscu, przechowuj zapytanie w obiekcie ViewModelSavedStateHandle. 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 odtwarzana po zatrzymaniu przez system. System ma zapisane zapytanie w zapisanej instancji stanu pakietu, a interfejs użytkownika powinien przekazać zapytanie do
ViewModel, jeśli nie jest używana funkcjaSavedStateHandle.ViewModelwidzi, że nie ma wyników wyszukiwania w pamięci podręcznej, i przekazuje zadanie wczytania wyników wyszukiwania za pomocą podanego zapytania. - Po zmianie konfiguracji aktywność jest odtwarzana. Ponieważ instancja
ViewModelnie została zniszczona, ma ona wszystkie informacje zapisane w pamięci podręcznej i nie musi ponownie wysyłać zapytań do bazy danych.ViewModel
Dodatkowe materiały
Więcej informacji o zapisywaniu stanów interfejsu znajdziesz w tych materiałach.
Blogi
- ViewModels: prosty przykład
- ViewModel: trwałość,
onSaveInstanceState, Przywracanie stanu interfejsu i ładowarek