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, rememberSaveable i rememberSerializable. 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:
|
|
|
|
|---|---|---|---|
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 ( |
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 |
Przypadki użycia |
|
|
|
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
rememberSaveable i rememberSerializable 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 rememberSaveable i rememberSerializable 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),Stringlub 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 remember a rememberSaveable/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() } // ... }
retain a ViewModel
Obie te funkcje retain i ViewModel oferują podobne możliwości w najczęściej używanym zakresie, czyli utrzymywanie instancji obiektów po zmianach konfiguracji. Wybór między retain a ViewModel 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.
|
|
|
|---|---|---|
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 |
Destruction |
Gdy element trwale opuszcza hierarchię kompozycji |
Kiedy |
Dodatkowe funkcje |
Może otrzymywać wywołania zwrotne, gdy obiekt znajduje się w hierarchii kompozycji lub nie. |
Wbudowany |
Właściciel |
|
|
Use cases |
|
|
Połącz retain i rememberSaveable lub rememberSerializable
Czasami obiekt musi mieć hybrydowy okres istnienia, który łączy retained 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żesz używać jednocześnie retain i rememberSaveable 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 ListDetailPaneScaffold i NavDisplay (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. InstancjeMovableContentmogą 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 jestretained, a interfejs API nigdy nie będzie korzystać z innej odmianyremember, użyj zamiast tego prefiksuretain. - Użyj właściwości
rememberSaveablelubrememberSerializable, jeśli wybrano zachowywanie stanu i możliwe jest napisanie prawidłowej implementacjiSaver. - 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) } ) }