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ą użytkownikowi swój stan. Oto kilka przykładów stanu w aplikacjach na Androida:
- Snackbar wyświetlany, gdy nie można nawiązać połączenia z siecią.
- post na blogu i powiązane z nim komentarze;
- animacji rozchodzących się fal na przyciskach, które są odtwarzane, gdy użytkownik je kliknie;
- Naklejki, po których użytkownik może rysować na obrazie.
Jetpack Compose pomaga w jednoznaczny sposób określać, gdzie i jak przechowywać i używać stanu w aplikacji na Androida. Ten przewodnik koncentruje się na połączeniu między stanem a komponentami kompozycyjnymi oraz na interfejsach API, które Jetpack Compose udostępnia do pracy ze stanem.
Stan i skład
Compose jest deklaratywny, więc jedynym sposobem na jego aktualizację jest wywołanie tego samego komponentu z nowymi argumentami. Te argumenty reprezentują stan interfejsu. Za każdym razem, gdy stan jest aktualizowany, następuje rekompozycja. W rezultacie takie elementy jak TextField nie są automatycznie aktualizowane, jak ma to miejsce w przypadku widoków opartych na imperatywnym XML-u. Aby komponent mógł się odpowiednio zaktualizować, musi otrzymać wyraźną 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ż usługa TextField nie aktualizuje się sama – aktualizuje się, gdy zmienia się jej parametr value. Wynika to ze sposobu, w jaki w Compose działają kompozycja i ponowna kompozycja.
Początkowa kompozycja: utworzenie kompozycji przez pierwsze uruchomienie funkcji kompozycyjnych.
Ponowne komponowanie: ponowne uruchamianie funkcji kompozycyjnych w celu zaktualizowania kompozycji, gdy zmienią się dane.
Więcej informacji o początkowej kompozycji i ponownej kompozycji znajdziesz w artykule Myślenie w Compose.
Stan w funkcjach kompozycyjnych
Funkcje kompozycyjne mogą używać interfejsu remember API do przechowywania obiektu w pamięci. Wartość obliczona przez remember jest przechowywana w obiekcie Composition podczas początkowej kompozycji, a przechowywana wartość jest zwracana podczas ponownej kompozycji. remember może służyć do przechowywania zarówno obiektów modyfikowalnych, jak i niemodyfikowalnych.
mutableStateOf tworzy obiekt observableMutableState<T>, który jest typem observable zintegrowanym z czasem działania kompozycji.
interface MutableState<T> : State<T> {
override var value: T
}
Wszelkie zmiany w value harmonogramie ponownego komponowania dowolnych funkcji kompozycyjnych, które odczytują value.
Obiekt MutableState w funkcji kompozycyjnej można zadeklarować 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 stanowią uproszczenie składni dla różnych zastosowań stanu. Wybierz ten, który generuje najbardziej czytelny kod w funkcji kompozycyjnej, którą piszesz.
Składnia delegowania by wymaga tych importów:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Zapamiętaną wartość możesz wykorzystać jako parametr innych funkcji kompozycyjnych, a nawet jako logikę w instrukcjach, aby zmieniać wyświetlane funkcje 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") } ) } }
remember pomaga zachować stan podczas ponownego komponowania, ale nie zachowuje go podczas zmian konfiguracji. W tym celu musisz użyć rememberSaveable. 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 obserwowane. Przed odczytaniem w Compose innego typu obserwowalnego musisz przekonwertować go na State<T>, aby komponenty kompozycyjne mogły automatycznie ponownie się komponować, gdy zmieni się stan.
Compose zawiera funkcje do tworzenia State<T> z popularnych typów obserwowalnych używanych w aplikacjach na Androida. Zanim zaczniesz korzystać z tych integracji, dodaj odpowiednie artefakty w ten sposób:
Flow:collectAsStateWithLifecycle()collectAsStateWithLifecycle()zbiera wartości zFloww sposób uwzględniający cykl życia, dzięki czemu aplikacja może oszczędzać zasoby. Jest to ostatnia wartość wyemitowana z funkcji ComposeState. Ten interfejs API jest zalecanym sposobem 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ż też pobiera wartości zFlowi przekształca je wStateCompose.Zamiast
collectAsStateWithLifecycle, które jest używane tylko w przypadku Androida, 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()zaczyna obserwować tenLiveDatai przedstawia jego wartości za pomocąState.W pliku
build.gradlewymagana jest ta zależność:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.12.0")
}
Dynamiczny
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.12.0"
}
-
subscribeAsState()to funkcje rozszerzeń, 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.12.0")
}
Dynamiczny
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.12.0"
}
-
subscribeAsState()to funkcje rozszerzeń, 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.12.0")
}
Dynamiczny
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.12.0"
}
Stanowe i bezstanowe
Kompozycja, która używa remember do przechowywania obiektu, tworzy stan wewnętrzny, dzięki czemu jest stanowa. HelloContent to przykład funkcji kompozycyjnej z zachowywaniem stanu, ponieważ przechowuje i modyfikuje swój stan name wewnętrznie. Może to być przydatne w sytuacjach, w których dzwoniący nie musi kontrolować stanu i może używać go bez konieczności samodzielnego zarządzania stanem. Funkcje kompozycyjne ze stanem wewnętrznym są jednak mniej wielokrotnego użytku i trudniejsze do testowania.
Bezstanowy komponent kompozycyjny to komponent, który nie przechowuje żadnego stanu. Częstym sposobem na osiągnięcie stanu bezstanowego jest użycie przenoszenia stanu.
Podczas tworzenia funkcji kompozycyjnych wielokrotnego użytku często warto udostępniać zarówno stanową, jak i bezstanową wersję tej samej funkcji kompozycyjnej. Wersja stanowa jest wygodna dla wywołujących, którym nie zależy na stanie, a wersja bezstanowa jest niezbędna dla wywołujących, którzy muszą kontrolować lub podnosić stan.
Przenoszenie stanu
Podnoszenie stanu w Compose to wzorzec przenoszenia stanu do wywołującego funkcji kompozycyjnej, aby uczynić ją bezstanową. 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 powoduje zmianę wartości, gdzieTto proponowana nowa wartość.
Nie musisz jednak ograniczać się do onValueChange. Jeśli w przypadku komponentu można użyć bardziej szczegółowych zdarzeń, zdefiniuj je za pomocą wyrażeń lambda.
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 kompozycje stanowe mogą modyfikować swój stan. Jest to całkowicie wewnętrzne.
- Możliwość udostępniania: stan przeniesiony można udostępniać wielu funkcjom kompozycyjnym. Jeśli chcesz odczytać wartość
namew innym komponencie, możesz to zrobić dzięki przenoszeniu stanu. - Możliwość przechwytywania: osoby dzwoniące do funkcji kompozycyjnych bezstanowych mogą zignorować lub zmodyfikować zdarzenia przed zmianą stanu.
- Odłączone: stan kompozycji bezstanowych może być przechowywany w dowolnym miejscu. Możesz teraz na przykład przenieść
namedoViewModel.
W tym przykładzie wyodrębnisz name i onValueChange z HelloContent i przeniesiesz je w górę drzewa do funkcji kompozycyjnej HelloScreen, która 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") }) } }
Wyodrębnienie stanu z HelloContent ułatwia zrozumienie funkcji kompozycyjnej, ponowne użycie jej w różnych sytuacjach i testowanie. HelloContent jest niezależny od sposobu przechowywania jego stanu. Oznacza to, że jeśli zmodyfikujesz lub zastąpisz HelloScreen, nie musisz zmieniać sposobu implementacji HelloContent.
Wzorzec, w którym stan się zmniejsza, a zdarzenia rosną, nazywa się jednokierunkowym przepływem danych. W tym przypadku stan przechodzi z HelloScreen na HelloContent, a zdarzenia z HelloContent na HelloScreen. Dzięki jednokierunkowemu przepływowi danych możesz oddzielić komponenty kompozycyjne, które wyświetlają stan w interfejsie, od części aplikacji, które przechowują i zmieniają stan.
- Stan powinien być przeniesiony co najmniej do najniższego wspólnego elementu nadrzędnego wszystkich funkcji kompozycyjnych, które go używają (odczytują).
- Stan powinien być przeniesiony co najmniej na najwyższy poziom, na którym może zostać zmieniony (zapis).
- Jeśli dwa stany zmieniają się w odpowiedzi na te same zdarzenia, powinny być przenoszone razem.
Możesz przenieść stan wyżej, niż wymagają tego te reguły, ale przeniesienie stanu niżej utrudnia lub uniemożliwia śledzenie jednokierunkowego przepływu danych.
Więcej informacji znajdziesz na stronie Gdzie umieścić 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 adnotacji @Parcelize. Obiekt staje się możliwy do przekazania i można go połączyć w pakiet. Na przykład ten kod tworzy typ danych City, który można przekazywać między procesami, 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 funkcja @Parcelize nie jest odpowiednia, możesz użyć funkcji 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 uniknąć konieczności definiowania kluczy mapy, możesz też użyć listSaver
i użyć jej 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")) } }
Zmienne stanu w Compose
Proste przenoszenie stanu można zarządzać w samych funkcjach kompozycyjnych. Jeśli jednak ilość stanu do śledzenia wzrośnie lub pojawi się logika do wykonania w funkcjach kompozycyjnych, warto przekazać logikę i odpowiedzialność za stan do innych klas: obiektów przechowujących stan.
Pamiętaj, że w innych materiałach obiekty stanu są też nazywane obiektami stanu przeniesionego.
Więcej informacji znajdziesz w dokumentacji podnoszenia stanu w Compose lub na stronie Obiekty przechowujące stan i stan interfejsu w przewodniku po architekturze.
Ponowne uruchamianie obliczeń zapamiętywania po zmianie kluczy
Interfejs remember API jest często używany w połączeniu z interfejsem MutableState:
var name by remember { mutableStateOf("") }
W tym przypadku użycie funkcji remember sprawia, że wartość MutableState przetrwa
ponowne kompozycje.
Ogólnie rzecz biorąc, funkcja remember przyjmuje parametr lambda calculation. Gdy funkcja remember
jest uruchamiana po raz pierwszy, wywołuje funkcję lambda calculation i zapisuje jej wynik. Podczas rekompozycji funkcja remember zwraca ostatnio zapisaną wartość.
Oprócz stanu buforowania możesz też używać adnotacji remember do przechowywania w kompozycji dowolnego obiektu lub wyniku działania, którego inicjowanie lub obliczanie jest kosztowne. Możesz nie chcieć powtarzać tego obliczenia przy każdej ponownej kompozycji.
Przykładem może być utworzenie tego obiektu ShaderBrush, które jest kosztowną operacją:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember przechowuje wartość do momentu, gdy 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 ulegnie zmianie, przy następnym remember ponownym wygenerowaniu funkcji pamięć podręczna zostanie unieważniona, a blok lambda obliczeń zostanie ponownie wykonany. Ten mechanizm pozwala kontrolować czas życia obiektu w kompozycji. Obliczenia pozostają 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 element ShaderBrush, który jest używany jako kolor tła funkcji kompozycyjnej Box. remember przechowuje instancję ShaderBrush, ponieważ jej ponowne utworzenie jest kosztowne, jak wyjaśniono wcześniej. remember przyjmuje avatarRes jako parametr key1, czyli wybrany obraz tła. Jeśli zmieni się avatarRes, pędzel ponownie utworzy kompozycję z nowego obrazu i zastosuje ją 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 przechowującej stan
MyAppState. Udostępnia funkcję rememberMyAppState do inicjowania instancji klasy za pomocą remember. Udostępnianie takich funkcji w celu utworzenia instancji, która przetrwa ponowne kompozycje, to typowy wzorzec w Compose. Funkcja rememberMyAppState otrzymuje windowSizeClass, który służy jako parametr key dla funkcji remember. Jeśli ten parametr ulegnie zmianie, aplikacja musi ponownie utworzyć klasę zwykłego stanu z najnowszą wartością. Może się tak zdarzyć, gdy np. 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 metody equals klasy, aby określić, czy klucz uległ zmianie, i unieważnić zapisaną wartość.
Przechowywanie stanu za pomocą kluczy poza ponownym komponowaniem
Interfejs rememberSaveable API to otoka interfejsu remember, która może przechowywać dane w Bundle. Ten interfejs API umożliwia zachowanie stanu nie tylko podczas ponownego komponowania, ale też podczas ponownego tworzenia aktywności i zakończenia procesu zainicjowanego przez system.
rememberSaveable otrzymuje input parametry o tym samym celu, co remember otrzymuje keys. Pamięć podręczna jest unieważniana, gdy zmieni się którykolwiek z danych wejściowych. Przy następnym ponownym skomponowaniu funkcji rememberSaveable ponownie wykona
blok lambda obliczeń.
W przykładzie poniżej zmienna rememberSaveable przechowuje wartość userTypedQuery do momentu, gdy zmieni się wartość zmiennej 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 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