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 kolejnych kompozycjach, co zostało opisane 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 w różnych kompozycjach 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?

Wartości są zachowywane po ponownym utworzeniu 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

Wszystko

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 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 ponownie wykonany, ale wszystkie wywołania remember zwrócą wartości z poprzedniej kompozycji zamiast ponownie wykonywać obliczenia.

Każda instancja funkcji kompozycyjnej 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 key composable 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. Dlatego najlepiej sprawdza się w przypadku:

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

Należy jednak unikać:

  • Przechowywanie danych wprowadzonych przez użytkownika za pomocą remember, ponieważ zapamiętane obiekty są zapominane po zmianach konfiguracji aktywności i zakończeniu procesu zainicjowanego przez system.

rememberSaveablerememberSerializable

rememberSaveablerememberSerializable są oparte na remember. Mają one najdłuższą żywotność spośród funkcji zapamiętywania omówionych w tym przewodniku. Oprócz zapamiętywania pozycji obiektów w różnych kompozycjach może też zapisywać wartości, aby można je było przywracać po ponownym utworzeniu aktywności, w tym po zmianach konfiguracji i zakończeniu 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 działającej w tle).

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, takiego jak baza 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 złożone 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ą też 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 jest odpowiednikiem zapomnienia).

W zamian za ten krótszy niż rememberSaveable cykl życia funkcja retain może zachowywać 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 do zarządzania odtwarzaczem multimediów (np. ExoPlayer), aby zapobiec 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() }
    // ...
}

retain kontra ViewModel

Zarówno retain, jak i ViewModel oferują podobne funkcje w najczęściej używanej możliwości zachowywania 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.

ViewModels 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 pozostanie aktywne 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, a retain to dobry kandydat do przechowywania obiektów związanych z interfejsem, takich jak pamięci podręczne, śledzenie wyświetleń i dane analityczne, zależności od AndroidView i inne obiekty, które wchodzą w interakcje z systemem operacyjnym Android 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 zaleceniami dotyczącymi nowoczesnej architektury aplikacji na Androida, mogą też używać retain do tworzenia wewnętrznego interfejsu API „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

Zakres

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 innym miejscu zawsze działa na nowej instancji.

Elementy typu ViewModel są singletonami w ramach ViewModelStore.

Zniszczenie

Gdy element na stałe 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 komponentu 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 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 obejmuje zarówno retained, jak i rememberSaveable 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żna używać jednocześnie retainrememberSaveable lub rememberSerializable. Prawidłowe połączenie obu cykli życia jest dość skomplikowane. 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 obiektów zachowanych i zapisanych 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 nie można manipulować danymi zapisu za pomocą danych zachowywanych, ponieważ zapobiega to sytuacji, w której próbuje się zaktualizować dane zapisu, 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.

Pamięć podręczna 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, więc 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 zamiast przenosić obiekty w hierarchii kompozycji.
  • Użyj MovableContent, aby bezproblemowo 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, wiele obiektów bierze udział w tworzeniu i organizowaniu kompozycji. Najczęstszym przykładem tego typu elementów 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 Twojego stanu mogą bez obaw tworzyć instancje w hierarchii kompozycji, które przetrwają i zostaną unieważnione 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 zależeć od innej odmiany remember, użyj zamiast tego prefiksu retain.
  • Użyj właściwości rememberSaveable lub rememberSerializable, jeśli wybrano trwałość 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 użytkowania. Pamiętaj, że stan może być tworzony w innym miejscu niż to, w którym 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) }
    )
}