Stan w aplikacji to dowolna wartość, która może się zmieniać z upływem czasu. Jest to bardzo szeroka definicja, która obejmuje wszystko – od bazy danych Room po zmienną w klasie.
Wszystkie aplikacje na Androida wyświetlają stan użytkownikowi. Oto kilka przykładów stanu w aplikacjach na Androida:
- Snackbar, który wyświetla się, gdy nie można nawiązać połączenia z siecią.
- Post na blogu i powiązane z nim komentarze.
- Animacje falowania na przyciskach, które są odtwarzane, gdy użytkownik je kliknie.
- Naklejki, które użytkownik może narysować na obrazie.
Jetpack Compose pomaga Ci wyraźnie określić, gdzie i jak przechowujesz stan w aplikacji na Androida oraz jak go używasz. Ten przewodnik skupia się na powiązaniu między stanem a elementami kompozycyjnymi oraz na interfejsach API, które Jetpack Compose oferuje, aby ułatwić pracę ze stanem.
Stan i kompozycja
Compose jest deklaratywny, dlatego jedynym sposobem na jego aktualizację jest wywołanie tego samego elementu kompozycyjnego z nowymi argumentami. Te argumenty są reprezentacjami stanu interfejsu. Za każdym razem, gdy stan jest aktualizowany, następuje rekompozycja. W rezultacie elementy takie jak TextField nie aktualizują się automatycznie, jak to ma miejsce w przypadku imperatywnych widoków opartych na XML. Aby element kompozycyjny mógł się odpowiednio zaktualizować, musi wyraźnie otrzymać informację o nowym stanie.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
Jeśli uruchomisz ten kod i spróbujesz wpisać tekst, zobaczysz, że nic się nie dzieje. Dzieje się tak, ponieważ TextField nie aktualizuje się samodzielnie – aktualizuje się, gdy zmieni się jego parametr value. Wynika to ze sposobu, w jaki działa kompozycja i ponowne komponowanie w Compose.
Więcej informacji o początkowej kompozycji i ponownym komponowaniu znajdziesz w artykule Myślenie w Compose.
Stan w elementach kompozycyjnych
Funkcje kompozycyjne mogą używać interfejsu
remember
API do przechowywania obiektu w pamięci. Wartość obliczona przez remember jest przechowywana w kompozycji podczas początkowej kompozycji, a przechowywana wartość jest zwracana podczas ponownego komponowania.
Funkcji remember można używać do przechowywania zarówno obiektów zmiennych, jak i niezmiennych.
mutableStateOf
tworzy obserwowalny element
MutableState<T>,
który jest obserwowalnym typem zintegrowanym z środowiskiem wykonawczym Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Wszelkie zmiany w value powodują rekompozycję wszystkich funkcji typu „composable”, które odczytują value.
W elemencie kompozycyjnym można zadeklarować obiekt MutableState na 3 sposoby:
val mutableState = remember { mutableStateOf(default) }var value by remember { mutableStateOf(default) }val (value, setValue) = remember { mutableStateOf(default) }
Te deklaracje są równoważne i służą jako uproszczenie składni w przypadku różnych zastosowań stanu. Wybierz tę, która sprawi, że kod w pisanym przez Ciebie elemencie kompozycyjnym będzie najłatwiejszy do odczytania.
Składnia delegata by wymaga tych importów:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Zapamiętaną wartość możesz użyć jako parametru innych elementów kompozycyjnych, a nawet jako logiki w instrukcjach, aby zmienić wyświetlane elementy kompozycyjne. Jeśli na przykład nie chcesz wyświetlać powitania, gdy nazwa jest pusta, użyj stanu w instrukcji if:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
Funkcja remember pomaga zachować stan podczas rekompozycji, ale nie zachowuje stanu podczas zmian konfiguracji. W tym celu musisz użyć funkcji rememberSaveable. Funkcja rememberSaveable automatycznie zapisuje każdą wartość, którą można zapisać w Bundle. W przypadku innych wartości możesz przekazać niestandardowy obiekt zapisujący.
Inne obsługiwane typy stanu
Compose nie wymaga używania MutableState<T> do przechowywania stanu. Obsługuje inne typy obserwowalne. Zanim odczytasz inny typ obserwowalny w
Compose, musisz przekonwertować go na State<T> aby elementy kompozycyjne mogły
automatycznie ponownie się komponować, gdy zmieni się stan.
Compose zawiera funkcje tworzenia State<T> z typowych typów obserwowalnych
używanych w aplikacjach na Androida. Zanim zaczniesz korzystać z tych integracji, dodaj
odpowiednie artefakty zgodnie z opisem poniżej:
Flow:collectAsStateWithLifecycle()collectAsStateWithLifecycle()zbiera wartości zFloww sposób uwzględniający cykl życia, co pozwala aplikacji oszczędzać zasoby. Reprezentuje ona ostatnią wartość wyemitowaną przez ComposeState. Używaj tego interfejsu API jako zalecanego sposobu zbierania przepływów w aplikacjach na Androida.W pliku
build.gradlewymagana jest ta zależność (powinna to być wersja 2.6.0-beta01 lub nowsza):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
}
Dynamiczny
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.10.0"
}
-
collectAsStatejest podobne docollectAsStateWithLifecycle, ponieważ również zbiera wartości zFlowi przekształca je w ComposeState.Zamiast
collectAsStateWithLifecycle, która jest dostępna tylko w Androidzie, używajcollectAsStatew przypadku kodu niezależnego od platformy.W przypadku
collectAsStatenie są wymagane żadne dodatkowe zależności, ponieważ jest ona dostępna wcompose-runtime. -
observeAsState()rozpoczyna obserwowanie tegoLiveDatai reprezentuje jego wartości za pomocąState.W pliku
build.gradlewymagana jest ta zależność:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.11.0")
}
Dynamiczny
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.11.0"
}
-
subscribeAsState()to funkcje rozszerzające, które przekształcają strumienie reaktywne RxJava2 (np.Single,Observable,Completable) wStateCompose.W pliku
build.gradlewymagana jest ta zależność:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.11.0")
}
Dynamiczny
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.11.0"
}
-
subscribeAsState()to funkcje rozszerzające, które przekształcają strumienie reaktywne RxJava3 (np.Single,Observable,Completable) wStateCompose.W pliku
build.gradlewymagana jest ta zależność:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.11.0")
}
Dynamiczny
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.11.0"
}
Stanowy a bezstanowy
Element kompozycyjny, który używa remember do przechowywania obiektu, tworzy stan wewnętrzny, co sprawia, że element kompozycyjny jest stanowy. HelloContent to przykład elementu kompozycyjnego ze stanem, ponieważ przechowuje i modyfikuje swój stan name wewnętrznie. Może to być przydatne w sytuacjach, gdy wywołujący nie musi kontrolować stanu i może go używać bez konieczności samodzielnego zarządzania stanem. Jednak elementy kompozycyjne ze stanem wewnętrznym są zwykle mniej użyteczne i trudniejsze do testowania.
Bezstanowy element kompozycyjny to element, który nie przechowuje żadnego stanu. Łatwym sposobem na osiągnięcie bezstanowości jest użycie przenoszenia stanu hoisting.
Podczas tworzenia elementów kompozycyjnych wielokrotnego użytku często warto udostępnić zarówno stanową, jak i bezstanową wersję tego samego elementu kompozycyjnego. Wersja stanowa jest wygodna dla wywołujących, którzy nie dbają o stan, a wersja bezstanowa jest niezbędna dla wywołujących, którzy muszą kontrolować lub przenosić stan.
Przenoszenie stanu
Przenoszenie stanu w Compose to wzorzec przenoszenia stanu do wywołującego element kompozycyjny, aby element kompozycyjny był bezstanowy. Ogólny wzorzec przenoszenia stanu w Jetpack Compose polega na zastąpieniu zmiennej stanu 2 parametrami:
value: T:bieżąca wartość do wyświetlenia.onValueChange: (T) -> Unit: zdarzenie, które żąda zmiany wartości, gdzieTjest proponowaną nową wartością.
Nie musisz jednak ograniczać się do onValueChange. Jeśli dla elementu kompozycyjnego odpowiedniejsze są bardziej szczegółowe zdarzenia, zdefiniuj je za pomocą lambd.
Stan przenoszony w ten sposób ma kilka ważnych właściwości:
- Jedno źródło wiarygodnych danych: przenosząc stan zamiast go duplikować, zapewniamy, że istnieje tylko jedno źródło wiarygodnych danych. Pomaga to uniknąć błędów.
- Hermetyzacja: tylko elementy kompozycyjne ze stanem mogą modyfikować swój stan. Jest on całkowicie wewnętrzny.
- Udostępnianie: przeniesiony stan można udostępniać wielu elementom kompozycyjnym. Jeśli chcesz odczytać
namew innej funkcji kompozycyjnej, przenoszenie stanu Ci to umożliwi. - Przechwytywanie: wywołujący bezstanowe elementy kompozycyjne mogą ignorować lub modyfikować zdarzenia przed zmianą stanu.
- Oddzielenie: stan bezstanowych elementów kompozycyjnych może być przechowywany w dowolnym miejscu. Na przykład teraz można przenieść
namedoViewModel.
W tym przykładzie wyodrębnisz name i onValueChange z HelloContent i przeniesiesz je w górę drzewa do elementu kompozycyjnego HelloScreen, który wywołuje HelloContent.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
Dzięki przeniesieniu stanu z HelloContent łatwiej jest zrozumieć element kompozycyjny, używać go ponownie w różnych sytuacjach i testować. HelloContent jest oddzielony od sposobu przechowywania jego stanu. Oddzielenie oznacza, że jeśli zmodyfikujesz lub zastąpisz HelloScreen, nie musisz zmieniać sposobu implementacji HelloContent.
Wzorzec, w którym stan przechodzi w dół, a zdarzenia w górę, nazywa się jednokierunkowym przepływem danych. W tym przypadku stan przechodzi w dół z HelloScreen do HelloContent, a zdarzenia w górę z HelloContent do HelloScreen. Dzięki jednokierunkowemu przepływowi danych możesz oddzielić elementy kompozycyjne, które wyświetlają stan w interfejsie, od części aplikacji, które przechowują i zmieniają stan.
Więcej informacji znajdziesz na stronie Gdzie przenieść stan.
Przywracanie stanu w Compose
Interfejs rememberSaveable API działa podobnie do remember, ponieważ
zachowuje stan podczas ponownego komponowania, a także podczas ponownego tworzenia aktywności lub procesu
za pomocą mechanizmu zapisanego stanu instancji. Dzieje się tak na przykład, gdy ekran jest obracany.
Sposoby przechowywania stanu
Wszystkie typy danych dodane do Bundle są zapisywane automatycznie. Jeśli chcesz zapisać coś, czego nie można dodać do Bundle, masz kilka opcji.
Parcelize
Najprostszym rozwiązaniem jest dodanie do obiektu
@Parcelize
adnotacji. Obiekt staje się możliwy do spakowania i można go spakować. Ten kod sprawia na przykład, że typ danych City jest możliwy do spakowania i zapisuje go w stanie.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
Jeśli z jakiegoś powodu @Parcelize nie jest odpowiednie, możesz użyć mapSaver, aby zdefiniować własną regułę konwertowania obiektu na zestaw wartości, które system może zapisać w Bundle.
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
Aby nie trzeba było definiować kluczy mapy, możesz też użyć listSaver i użyć jego indeksów jako kluczy:
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
Kontenery stanu w Compose
Proste przenoszenie stanu można zarządzać w samych funkcjach kompozycyjnych. Jeśli jednak ilość stanu do śledzenia wzrośnie lub w funkcjach kompozycyjnych pojawi się logika do wykonania, warto przekazać obowiązki związane z logiką i stanem innym klasom – kontenerom stanu.
Więcej informacji znajdziesz w dokumentacji przenoszenia stanu w Compose lub ogólnie na stronie Kontenery stanu i stan interfejsu w przewodniku po architekturze.
Ponowne uruchamianie obliczeń pamięci po zmianie kluczy
Interfejs remember API jest często używany razem z MutableState:
var name by remember { mutableStateOf("") }
W tym przypadku użycie funkcji remember sprawia, że wartość MutableState przetrwa ponowne komponowanie.
Ogólnie rzecz biorąc, remember przyjmuje parametr lambda calculation. Gdy remember jest uruchamiana po raz pierwszy, wywołuje lambdę calculation i zapisuje jej wynik. Podczas ponownego komponowania remember zwraca ostatnio zapisaną wartość.
Oprócz buforowania stanu możesz też używać remember do przechowywania w kompozycji dowolnego obiektu lub wyniku operacji, których inicjowanie lub obliczanie jest kosztowne. Możesz nie chcieć powtarzać tego obliczenia podczas każdego ponownego komponowania.
Przykładem jest utworzenie tego ShaderBrush obiektu, który jest kosztowną
operacją:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
Funkcja remember przechowuje wartość do momentu, aż opuści ona kompozycję. Istnieje jednak sposób na unieważnienie wartości w pamięci podręcznej. Interfejs remember API przyjmuje też parametr key lub
keys. Jeśli którykolwiek z tych kluczy się zmieni, przy następnym ponownym komponowaniu funkcji, remember unieważni pamięć podręczną i ponownie wykona blok lambda obliczeń
. Ten mechanizm pozwala kontrolować czas życia obiektu w kompozycji. Obliczenie pozostaje ważne do momentu zmiany danych wejściowych, a nie do momentu, gdy zapamiętana wartość opuści kompozycję.
Poniższe przykłady pokazują, jak działa ten mechanizm.
W tym fragmencie kodu tworzony jest ShaderBrush, który jest używany jako tło
farba elementu kompozycyjnego Box. remember przechowuje instancję ShaderBrush
ponieważ jej ponowne utworzenie jest kosztowne, jak wyjaśniono wcześniej. Funkcja remember przyjmuje avatarRes jako parametr key1, który jest wybranym obrazem tła. Jeśli avatarRes się zmieni, pędzel ponownie się skomponuje z nowym obrazem i ponownie zastosuje do Box. Może się to zdarzyć, gdy użytkownik wybierze inny obraz jako tło w selektorze.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
W następnym fragmencie kodu stan jest przenoszony do zwykłej klasy zmiennej stanu
MyAppState. Udostępnia ona funkcję rememberMyAppState, która inicjuje instancję klasy za pomocą remember. Udostępnianie takich funkcji do tworzenia instancji, która przetrwa ponowne komponowanie, jest powszechnym wzorcem w Compose. Funkcja
rememberMyAppState otrzymuje windowSizeClass, która służy jako
parametr key dla remember. Jeśli ten parametr się zmieni, aplikacja musi ponownie utworzyć zwykłą klasę kontenera stanu z najnowszą wartością. Może się to zdarzyć, gdy na przykład użytkownik obróci urządzenie.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose używa implementacji klasy equals, aby określić, czy klucz się zmienił, i unieważnić zapisaną wartość.
Przechowywanie stanu za pomocą kluczy poza ponownym komponowaniem
Interfejs rememberSaveable API to otoka wokół remember, która może przechowywać
dane w Bundle. Ten interfejs API umożliwia zachowanie stanu nie tylko podczas rekompozycji, ale także podczas ponownego tworzenia aktywności i śmierci procesu zainicjowanego przez system.
rememberSaveable otrzymuje parametry input w tym samym celu, w jakim
remember otrzymuje keys. Pamięć podręczna jest unieważniana, gdy zmieni się którykolwiek z danych wejściowych. Przy następnym ponownym komponowaniu funkcji rememberSaveable ponownie wykona blok lambda obliczeń.
W tym przykładzie rememberSaveable przechowuje userTypedQuery do momentu zmiany typedQuery:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
Więcej informacji
Więcej informacji o stanie i Jetpack Compose znajdziesz w tych dodatkowych materiałach.
Przykłady
Codelabs
Filmy
Blogi
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Projektowanie interfejsu Compose
- Zapisywanie stanu interfejsu w Compose
- Efekty uboczne w Compose