Okresy istnienia stanu w Compose

W Jetpack Compose funkcje kompozycyjne często przechowują stan za pomocą funkcji remember. Zapamiętane wartości można ponownie wykorzystać w różnych kompozycjach, jak wyjaśniono w artykule Stan i Jetpack Compose.

remember służy do utrwalania wartości w różnych kompozycjach, ale stan często musi istnieć dłużej niż kompozycja. Na tej stronie wyjaśniamy różnice między interfejsami API remember, retain, rememberSaveable i rememberSerializable, kiedy należy wybrać dany interfejs API oraz jakie są sprawdzone metody zarządzania zapamiętanymi i zachowanymi wartościami w Compose.

Wybierz prawidłowy okres eksploatacji

W Compose możesz używać kilku funkcji, aby zachowywać stan między kompozycjami i poza nimi: remember, retain, rememberSaveablerememberSerializable. Funkcje te różnią się okresem istnienia i semantyką, a każda z nich jest odpowiednia do przechowywania określonych rodzajów stanu. Różnice zostały opisane w tabeli poniżej:

remember

retain

rememberSaveable, rememberSerializable

Czy wartości przetrwają ponowne kompozycje?

Czy wartości przetrwają ponowne utworzenie aktywności?

Zawsze będzie zwracana ta sama instancja (===).

Zostanie zwrócony równoważny obiekt (==), prawdopodobnie zdeserializowana kopia.

Czy wartości przetrwają śmierć procesu?

Obsługiwane typy danych

Wszystkie

Nie może odwoływać się do żadnych obiektów, które zostałyby ujawnione, gdyby aktywność została zniszczona.

Musi być serializowalny
(za pomocą niestandardowego elementu Saver lub elementu kotlinx.serialization)

Przypadki użycia

  • Obiekty, które są ograniczone do kompozycji
  • Obiekty konfiguracji dla funkcji kompozycyjnych
  • Stan, który można odtworzyć bez utraty wierności interfejsu
  • Pamięci podręczne
  • Obiekty długotrwałe lub „zarządzające”
  • Dane wejściowe użytkownika
  • Stan, którego aplikacja nie może odtworzyć, np. dane wpisane w polu tekstowym, stan przewijania, przełączniki itp.

remember

remember to najczęstszy sposób przechowywania stanu w Compose. Gdy funkcja remember zostanie wywołana po raz pierwszy, podane obliczenia zostaną wykonane i zapamiętane, co oznacza, że zostaną zapisane przez Compose do ponownego użycia przez funkcję kompozycyjną. Gdy funkcja kompozycyjna zostanie ponownie skomponowana, jej kod zostanie wykonany ponownie, ale wszystkie wywołania remember zwrócą wartości z poprzedniej kompozycji zamiast ponownie wykonywać obliczenia.

Każda instancja funkcji typu „composable” ma własny zestaw zapamiętanych wartości, co określa się jako zapamiętywanie pozycyjne. Zapamiętane wartości są zapamiętywane do użycia w różnych kompozycjach i są powiązane z ich pozycją w hierarchii kompozycji. Jeśli funkcja kompozycyjna jest używana w różnych miejscach, każda jej instancja w hierarchii kompozycji ma własny zestaw zapamiętanych wartości.

Gdy zapamiętana wartość nie jest już używana, jest zapominana, a jej rekord jest odrzucany. Zapamiętane wartości są zapominane, gdy zostaną usunięte z hierarchii kompozycji (w tym gdy wartość zostanie usunięta i ponownie dodana w celu przeniesienia w inne miejsce bez użycia funkcji kompozycyjnej key lub MovableContent) lub gdy zostaną wywołane z innymi parametrami key.

Spośród dostępnych opcji funkcja remember ma najkrótszy okres ważności i najszybciej zapomina wartości spośród 4 funkcji zapamiętywania opisanych na tej stronie. Dzięki temu najlepiej nadaje się do:

  • tworzenie wewnętrznych obiektów stanu, takich jak pozycja przewijania lub stan animacji;
  • Unikanie kosztownego odtwarzania obiektu przy każdej ponownej kompozycji

Należy jednak unikać:

  • Przechowywanie danych wejściowych użytkownika za pomocą remember, ponieważ zapamiętane obiekty są zapominane po zmianach konfiguracji Activity i śmierci procesu zainicjowanej przez system.

rememberSaveable i rememberSerializable

rememberSaveablerememberSerializable są oparte na remember. Mają one najdłuższy okres eksploatacji spośród funkcji zapamiętywania omówionych w tym przewodniku. Oprócz zapamiętywania obiektów w różnych rekompozycjach może też zapisywać wartości, aby można było je przywracać po ponownym utworzeniu aktywności, w tym po zmianach konfiguracji i śmierci procesu (gdy system zamyka proces aplikacji działającej w tle, zwykle w celu zwolnienia pamięci dla aplikacji działających na pierwszym planie lub gdy użytkownik cofnie uprawnienia aplikacji podczas jej działania).

rememberSerializable działa tak samo jak rememberSaveable, ale automatycznie obsługuje utrwalanie złożonych typów, które można serializować za pomocą biblioteki kotlinx.serialization. Wybierz rememberSerializable, jeśli Twój typ jest (lub może być) oznaczony symbolem @Serializable, a w pozostałych przypadkach wybierz rememberSaveable.

Dzięki temu zarówno rememberSaveable, jak i rememberSerializable doskonale nadają się do przechowywania stanu związanego z danymi wejściowymi użytkownika, w tym wpisów w polu tekstowym, pozycji przewijania, stanów przełączników itp. Ten stan należy zapisać, aby użytkownik nigdy nie stracił swojego miejsca. Ogólnie rzecz biorąc, należy używać rememberSaveable lub rememberSerializable do zapamiętywania stanu, którego aplikacja nie może pobrać z innego trwałego źródła danych, np. z bazy danych.

Pamiętaj, że funkcje rememberSaveablerememberSerializable zapisują zapamiętane wartości, serializując je do postaci Bundle. Ma to 2 konsekwencje:

  • Wartości, które chcesz zapamiętać, muszą być reprezentowane przez co najmniej jeden z tych typów danych: typy proste (w tym Int, Long, Float, Double), String lub tablice dowolnego z tych typów.
  • Gdy zapisana wartość zostanie przywrócona, będzie to nowa instancja równa ==, ale nie ten sam odnośnik ===, którego kompozycja używała wcześniej.

Aby przechowywać bardziej skomplikowane typy danych bez używania kotlinx.serialization, możesz wdrożyć niestandardowy Saver, który będzie serializować i deserializować obiekt do obsługiwanych typów danych. Pamiętaj, że Compose od razu rozpoznaje typowe typy danych, takie jak State, List, Map, Set itp., i automatycznie przekształca je w obsługiwane typy. Poniżej znajdziesz przykład Saver dla klasy Size. Jest ona implementowana przez spakowanie wszystkich właściwości Size do listy za pomocą listSaver.

data class Size(val x: Int, val y: Int) {
    object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver(
        save = { listOf(it.x, it.y) },
        restore = { Size(it[0], it[1]) }
    )
}

@Composable
fun rememberSize(x: Int, y: Int) {
    rememberSaveable(x, y, saver = Size.Saver) {
        Size(x, y)
    }
}

retain

Interfejs retain API znajduje się pomiędzy rememberrememberSaveable/rememberSerializable pod względem czasu, przez jaki zapamiętuje wartości. Ma inną nazwę, ponieważ zachowane wartości mają inny cykl życia niż ich zapamiętane odpowiedniki.

Gdy wartość jest zachowywana, jest ona zapamiętywana pozycyjnie i zapisywana w dodatkowej strukturze danych, która ma oddzielny okres istnienia powiązany z okresem istnienia aplikacji. Zachowana wartość może przetrwać zmiany konfiguracji bez serializacji, ale nie może przetrwać zakończenia procesu. Jeśli wartość nie jest używana po ponownym utworzeniu hierarchii kompozycji, zachowana wartość jest wycofywana (co w przypadku retain oznacza zapomnienie).

W zamian za ten krótszy niż rememberSaveable cykl życia retain może przechowywać wartości, których nie można serializować, takie jak wyrażenia lambda, przepływy i duże obiekty, np. mapy bitowe. Możesz na przykład użyć retain, aby zarządzać odtwarzaczem multimediów (np. ExoPlayerem) i zapobiegać przerwom w odtwarzaniu multimediów podczas zmiany konfiguracji.

@Composable
fun MediaPlayer() {
    // Use the application context to avoid a memory leak
    val applicationContext = LocalContext.current.applicationContext
    val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() }
    // ...
}

retainViewModel

Obie te funkcje retainViewModel oferują podobne możliwości w najczęściej używanym zakresie, czyli utrzymywanie instancji obiektów po zmianach konfiguracji. Wybór między retainViewModel zależy od rodzaju przechowywanej wartości, zakresu, w jakim ma być ona dostępna, oraz od tego, czy potrzebujesz dodatkowych funkcji.

ViewModel to obiekty, które zwykle obejmują komunikację między interfejsem aplikacji a warstwami danych. Umożliwiają one przeniesienie logiki z funkcji kompozycyjnych, co zwiększa możliwość testowania. ViewModel są zarządzane jako singletony w ramach ViewModelStore i mają inny okres istnienia niż zachowane wartości. ViewModel pozostaje aktywny do momentu zniszczenia jego ViewModelStore, ale zachowane wartości są wycofywane, gdy treść zostanie trwale usunięta z kompozycji (np. w przypadku zmiany konfiguracji oznacza to, że zachowana wartość jest wycofywana, jeśli hierarchia interfejsu zostanie ponownie utworzona, a zachowana wartość nie została użyta po ponownym utworzeniu kompozycji).

ViewModel obejmuje też gotowe integracje do wstrzykiwania zależności za pomocą Daggera i Hilta, integrację z SavedState oraz wbudowaną obsługę współprogramów do uruchamiania zadań w tle. Dzięki temu ViewModel jest idealnym miejscem do uruchamiania zadań w tle i żądań sieciowych, interakcji z innymi źródłami danych w projekcie oraz opcjonalnego przechwytywania i utrwalania kluczowego stanu interfejsu, który powinien być zachowywany podczas zmian konfiguracji w ViewModel i przetrwać zakończenie procesu.

retain najlepiej nadaje się do obiektów, które są ograniczone do konkretnych instancji funkcji kompozycyjnych i nie wymagają ponownego użycia ani udostępniania między funkcjami kompozycyjnymi tego samego poziomu. gdzie ViewModel jest dobrym miejscem do przechowywania stanu interfejsu i wykonywania zadań w tle, retain to dobry kandydat do przechowywania obiektów związanych z interfejsem, takich jak pamięć podręczna, śledzenie wyświetleń i dane analityczne, zależności od AndroidView i inne obiekty, które wchodzą w interakcje z systemem operacyjnym Androida lub zarządzają bibliotekami innych firm, takimi jak procesory płatności czy reklamy.

Zaawansowani użytkownicy, którzy projektują niestandardowe wzorce architektury aplikacji poza retainzaleceniami dotyczącymi nowoczesnej architektury aplikacji na Androidaretain, mogą też używać retain do tworzenia wewnętrznego interfejsu API „podobnego do retain”.ViewModel Chociaż obsługa korutyn i zapisanego stanu nie jest dostępna od razu, retain może służyć jako element składowy cyklu życia podobnych do ViewModel obiektów z tymi funkcjami. Szczegóły projektowania takiego komponentu wykraczają poza zakres tego przewodnika.

retain

ViewModel

Określanie zakresu

Brak wartości wspólnych; każda wartość jest zachowywana i powiązana z określonym punktem w hierarchii kompozycji. Zachowanie tego samego typu w innej lokalizacji zawsze działa na nowej instancji.

Elementy typu ViewModel są pojedynczymi instancjami w ramach ViewModelStore.

Destruction

Gdy element trwale opuszcza hierarchię kompozycji

Kiedy ViewModelStore zostanie wyczyszczony lub zniszczony

Dodatkowe funkcje

Może otrzymywać wywołania zwrotne, gdy obiekt znajduje się w hierarchii kompozycji lub nie.

Wbudowany coroutineScope, obsługa SavedStateHandle, można wstrzykiwać za pomocą Hilt

Właściciel

RetainedValuesStore

ViewModelStore

Use cases

  • Utrzymywanie wartości specyficznych dla interfejsu w poszczególnych instancjach funkcji kompozycyjnych
  • Śledzenie wyświetleń, być może za pomocą RetainedEffect
  • Element składowy do definiowania niestandardowej architektury podobnej do ViewModel
  • wyodrębnianie interakcji między interfejsem a warstwami danych do osobnej klasy, zarówno w celu uporządkowania kodu, jak i przeprowadzania testów;
  • przekształcanie obiektów Flow w obiekty State i wywoływanie funkcji zawieszania, których nie powinny przerywać zmiany konfiguracji;
  • Udostępnianie stanów na dużych obszarach interfejsu, np. na całych ekranach
  • Interoperacyjność z View

Połącz retainrememberSaveable lub rememberSerializable

Czasami obiekt musi mieć hybrydowy okres istnienia, który łączy retainedrememberSaveable lub rememberSerializable. Może to oznaczać, że Twój obiekt powinien być ViewModel, który może obsługiwać zapisany stan zgodnie z opisem w module Zapisany stan w przewodniku po ViewModelu.

Możesz używać jednocześnie retainrememberSaveable lub rememberSerializable. Prawidłowe połączenie obu cykli życia znacznie zwiększa złożoność. Zalecamy stosowanie tego wzorca w ramach bardziej zaawansowanych i niestandardowych wzorców architektury oraz tylko wtedy, gdy spełnione są wszystkie te warunki:

  • Definiujesz obiekt składający się z wartości, które muszą zostać zachowane lub zapisane (np.obiekt śledzący dane wejściowe użytkownika i pamięć podręczną w pamięci, której nie można zapisać na dysku).
  • Stan jest ograniczony do funkcji kompozycyjnej i nie nadaje się do zakresu singletona ani okresu istnienia ViewModel

W takich przypadkach zalecamy podzielenie klasy na 3 części: zapisane dane, zachowane dane i obiekt „pośredniczący”, który nie ma własnego stanu i przekazuje do zachowanych i zapisanych obiektów informacje o aktualizacji stanu. Ten wzorzec ma następującą postać:

@Composable
fun rememberAndRetain(): CombinedRememberRetained {
    val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) {
        ExtractedSaveData()
    }
    val retainData = retain { ExtractedRetainData() }
    return remember(saveData, retainData) {
        CombinedRememberRetained(saveData, retainData)
    }
}

@Serializable
data class ExtractedSaveData(
    // All values that should persist process death should be managed by this class.
    var savedData: AnotherSerializableType = defaultValue()
)

class ExtractedRetainData {
    // All values that should be retained should appear in this class.
    // It's possible to manage a CoroutineScope using RetainObserver.
    // See the full sample for details.
    var retainedData = Any()
}

class CombinedRememberRetained(
    private val saveData: ExtractedSaveData,
    private val retainData: ExtractedRetainData,
) {
    fun doAction() {
        // Manipulate the retained and saved state as needed.
    }
}

Dzięki rozdzieleniu stanu według okresu istnienia podział obowiązków i pamięci staje się bardzo wyraźny. Celowo uniemożliwiamy manipulowanie danymi zapisu za pomocą danych zachowywanych, ponieważ zapobiega to sytuacji, w której próba aktualizacji danych zapisu jest podejmowana, gdy pakiet savedInstanceState został już przechwycony i nie można go zaktualizować. Umożliwia też testowanie scenariuszy ponownego tworzenia przez testowanie konstruktorów bez wywoływania funkcji Compose ani symulowania ponownego tworzenia aktywności.

Pełny przykład (RetainAndSaveSample.kt) pokazuje, jak można wdrożyć ten wzorzec.

Zapamiętywanie pozycji i układy adaptacyjne

Aplikacje na Androida mogą obsługiwać wiele rodzajów urządzeń, w tym telefony, urządzenia składane, tablety i komputery. Aplikacje często muszą przechodzić między tymi formatami, korzystając z układów adaptacyjnych. Na przykład aplikacja działająca na tablecie może wyświetlać widok listy i szczegółów w 2 kolumnach, ale na mniejszym ekranie telefonu może przełączać się między listą a stroną szczegółów.

Zapamiętane i zachowane wartości są zapamiętywane pozycyjnie, dlatego są ponownie używane tylko wtedy, gdy pojawiają się w tym samym miejscu w hierarchii kompozycji. Gdy układy dostosowują się do różnych formatów, mogą zmieniać strukturę hierarchii kompozycji i prowadzić do utraty wartości.

W przypadku gotowych komponentów, takich jak ListDetailPaneScaffoldNavDisplay (z Jetpack Navigation 3), nie stanowi to problemu, a stan będzie się utrzymywać podczas zmian układu. W przypadku komponentów niestandardowych, które dostosowują się do różnych formatów, zadbaj o to, aby zmiany układu nie wpływały na stan. Możesz to zrobić w jeden z tych sposobów:

  • Upewnij się, że kompozycje stanowe są zawsze wywoływane w tym samym miejscu w hierarchii kompozycji. Wdrażaj układy adaptacyjne, zmieniając logikę układu, a nie przenosząc obiektów w hierarchii kompozycji.
  • Użyj MovableContent, aby bez utraty danych przenieść komponenty z zachowywaniem stanu. Instancje MovableContent mogą przenosić zapamiętane i zachowane wartości ze starych do nowych lokalizacji.

Pamiętaj o funkcjach fabrycznych

Chociaż interfejsy Compose składają się z funkcji kompozycyjnych, w procesie tworzenia i organizowania kompozycji bierze udział wiele obiektów. Najczęstszym przykładem są złożone obiekty kompozycyjne, które definiują własny stan, np. LazyList, które akceptują LazyListState.

Podczas definiowania obiektów związanych z Compose zalecamy utworzenie funkcji remember, która określa zamierzone zachowanie zapamiętywania, w tym okres istnienia i kluczowe dane wejściowe. Dzięki temu użytkownicy stanu mogą bez obaw tworzyć instancje w hierarchii kompozycji, które będą trwałe i unieważniane zgodnie z oczekiwaniami. Podczas definiowania funkcji fabrycznej, którą można łączyć, postępuj zgodnie z tymi wskazówkami:

  • Dodaj przed nazwą funkcji prefiks remember. Opcjonalnie, jeśli implementacja funkcji zależy od tego, czy obiekt jest retained, a interfejs API nigdy nie będzie korzystać z innej odmiany remember, użyj zamiast tego prefiksu retain.
  • Użyj właściwości rememberSaveable lub rememberSerializable, jeśli wybrano zachowywanie stanu i możliwe jest napisanie prawidłowej implementacji Saver.
  • Unikaj skutków ubocznych lub inicjowania wartości na podstawie CompositionLocal, które mogą nie być istotne dla danego zastosowania. Pamiętaj, że stan może być tworzony w innym miejscu niż jest używany.

@Composable
fun rememberImageState(
    imageUri: String,
    initialZoom: Float = 1f,
    initialPanX: Int = 0,
    initialPanY: Int = 0
): ImageState {
    return rememberSaveable(imageUri, saver = ImageState.Saver) {
        ImageState(
            imageUri, initialZoom, initialPanX, initialPanY
        )
    }
}

data class ImageState(
    val imageUri: String,
    val zoom: Float,
    val panX: Int,
    val panY: Int
) {
    object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver(
        save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) },
        restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) }
    )
}