Stan w aplikacji to dowolna wartość, która może się zmieniać z czasem. Jest to bardzo ogólna 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:
- Pasek informacji, który pojawia się, gdy nie można nawiązać połączenia z siecią.
- Post na blogu i powiązane komentarze.
- Animacje falowania przycisków, które odtwarzają się, gdy użytkownik kliknie przycisk.
- Naklejki, które użytkownik może narysować na obrazie.
Jetpack Compose pomaga określić, gdzie i jak przechowywać stan oraz jak go używać w aplikacji na Androida. Ten przewodnik skupia się na związku między stanem a komponowalnymi komponentami oraz na interfejsach API, które Jetpack Compose udostępnia, aby ułatwić pracę ze stanem.
Stan i skład
Funkcja kompozytorna jest deklaratywna, więc jedynym sposobem na jej zaktualizowanie jest wywołanie tej samej funkcji kompozytowanej z nowymi argumentami. Te argumenty są reprezentacją stanu interfejsu użytkownika. Za każdym razem, gdy stan jest aktualizowany, następuje rekompozycja. W efekcie takie elementy jak TextField
nie są aktualizowane automatycznie, jak ma to miejsce w przypadku widoków na podstawie XML. Aby komponent mógł się odpowiednio zaktualizować, musi być poinformowany 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, nic się nie stanie. Dzieje się tak, ponieważ usługa TextField
nie aktualizuje się sama – aktualizuje się, gdy zmienia się parametr value
. Wynika to z tego, jak działają funkcje tworzenia i ponownego tworzenia w Compose.
Więcej informacji o pierwotnej i ponownej kompozycji znajdziesz w artykule Myślenie w komponowaniu.
Stan w funkcjach kompozytywnych
Funkcje składane mogą używać interfejsu remember
API do przechowywania obiektu w pamięci. Wartość obliczona przez remember
jest przechowywana w kompozycji podczas początkowego tworzenia kompozycji, a przechowywana wartość jest zwracana podczas ponownego tworzenia kompozycji.
remember
można używać do przechowywania zarówno obiektów zmiennych, jak i niezmiennych.
mutableStateOf
tworzy observable
MutableState<T>
,
który jest typem observable zintegrowanym z runtimem Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Wszelkie zmiany w value
powodują przekomponowanie wszystkich funkcji składanych, które odczytują value
.
Istnieją 3 sposoby deklarowania obiektu MutableState
w komponowalnym:
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 element ułatwiający stosowanie różnych stanów. Wybierz ten, który generuje kod najłatwiejszy do odczytania w komponowalnym elemencie, który piszesz.
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 komponentów lub nawet jako logiki w oświadczeniach, aby zmienić wyświetlane komponenty. Jeśli np. nie chcesz wyświetlać pozdrowienia, gdy nazwa jest pusta, użyj stanu w oświadczeniu 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 jest on zachowywany podczas zmian konfiguracji. W tym celu musisz użyć rememberSaveable
. rememberSaveable
automatycznie zapisuje wszystkie wartości, które można zapisać w Bundle
. W przypadku innych wartości możesz podać obiekt niestandardowego obiektu zapisu.
Inne obsługiwane typy stanów
W komponowaniu nie musisz używać MutableState<T>
do przechowywania stanu. Obsługuje ono inne obserwowalne typy. Zanim odczytasz inny typ obserwowalny w komponencie, musisz go przekonwertować na State<T>
, aby komponenty mogły się automatycznie ponownie skompilować po zmianie stanu.
Compose jest dostarczany z funkcjami umożliwiającymi tworzenie State<T>
na podstawie typów obserwowalnych powszechnie używanych w aplikacjach na Androida. Zanim użyjesz tych integracji, dodaj odpowiednie elementy w sposób opisany poniżej:
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
zbiera wartości zFlow
w sposób uwzględniający cykl życia, co pozwala aplikacji oszczędzać zasoby. Reprezentuje ona najnowszą wartość wyemitowaną przez funkcję ComposeState
. Zalecamy korzystanie z tego interfejsu API do zbierania danych w aplikacjach na Androida.W pliku
build.gradle
wymagana jest ta zależność (powinna to być wersja 2.6.0-beta01 lub nowsza):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
Funkcja
collectAsState
jest podobna do funkcjicollectAsStateWithLifecycle
, ponieważ również zbiera wartości z elementuFlow
i przekształca je w element ComposeState
.Zamiast
collectAsStateWithLifecycle
, który jest przeznaczony tylko na Androida, używaj kodu niezależnego od platformy (collectAsState
).W przypadku
collectAsState
nie są wymagane dodatkowe zależności, ponieważ jest ona dostępna wcompose-runtime
. -
observeAsState()
zaczyna obserwować ten parametrLiveData
i reprezentuje jego wartości za pomocą parametruState
.W pliku
build.gradle
wymagana jest ta zależność:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}
-
subscribeAsState()
to funkcje rozszerzenia, które przekształcają reaktywne strumienie RxJava2 (np.Single
,Observable
,Completable
) w ComposeState
.W pliku
build.gradle
wymagana jest ta zależność:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}
-
subscribeAsState()
to funkcje rozszerzenia, które przekształcają reaktywne strumienie RxJava3 (np.Single
,Observable
,Completable
) w ComposeState
.W pliku
build.gradle
wymagana jest ta zależność:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}
Stanowa a bezstanowa
Składowa, która używa remember
do przechowywania obiektu, tworzy stan wewnętrzny, dzięki czemu składowa staje się stanowa. HelloContent
jest przykładem kompozytu stanu, ponieważ przechowuje i modyfikuje stan name
wewnętrznie. Może to być przydatne w sytuacjach, gdy dzwoniący nie musi kontrolować stanu i może z niego korzystać bez konieczności samodzielnego nim zarządzać. Jednak komponenty z wewnętrznym stanem są zwykle mniej wielokrotnego użytku i trudniejsze do przetestowania.
Komponentem bezstanowym jest komponent, który nie przechowuje żadnego stanu. Prostym sposobem na osiągnięcie stanu bezstanowego jest użycie hoistingu stanu.
Podczas tworzenia wielokrotnego użytku komponentów często chcesz udostępnić wersję z stanem i bezstanową tego samego komponentu. Wersja ze stanem jest wygodna dla wywołujących, którym stan nie jest potrzebny, a wersja bezstanowa jest niezbędna dla wywołujących, którzy muszą kontrolować stan lub go podnosić.
Przenoszenie stanu
Przenoszenie stanu w komponowalnym to wzór przenoszenia stanu do wywołującego komponentu, aby uczynić go stanem bezstanowym. Ogólny wzorzec podnoszenia stanu w Jetpack Compose polega na zastąpieniu zmiennej stanu 2 parametrami:
value: T
: bieżąca wartość do wyświetleniaonValueChange: (T) -> Unit
: zdarzenie, które prosi o zmianę wartości, gdzieT
to proponowana nowa wartość
Nie musisz jednak ograniczać się do onValueChange
. Jeśli w przypadku kompozytowanego elementu odpowiednie są bardziej szczegółowe zdarzenia, zdefiniuj je za pomocą funkcji lambda.
Stan podniesiony w ten sposób ma kilka ważnych właściwości:
- Jedno źródło wiarygodnych danych: przeniesienie stanu zamiast jego powielania zapewnia, że istnieje tylko jedno źródło wiarygodnych danych. Pomaga to uniknąć błędów.
- Opakowane: stan mogą modyfikować tylko komponenty stateful. Jest to całkowicie wewnętrzna zmiana.
- Możliwość udostępniania: stan zasobów może być udostępniany wielu komponentom. Jeśli chcesz odczytać
name
w innej składanej, możesz to zrobić dzięki funkcji podnoszenia. - Możliwość przechwycenia: wywołujący bezstanowe komponenty mogą zignorować lub zmodyfikować zdarzenia przed zmianą stanu.
- Rozdzielenie: stan bezstanowych komponentów może być przechowywany w dowolnym miejscu. Teraz można na przykład przenieść
name
doViewModel
.
W tym przykładzie z komponentu HelloContent
wyodrębniasz elementy name
i onValueChange
, a potem przenosisz je wyżej w drzewie do kompozytowego komponentu HelloScreen
, który wywołuje komponent 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 wyodrębnieniu stanu z poziomu HelloContent
łatwiej jest analizować komponent, używać go ponownie w różnych sytuacjach i testować. HelloContent
jest odłączony od sposobu przechowywania stanu. Odłączenie oznacza, że jeśli zmodyfikujesz lub zastąpisz HelloScreen
, nie musisz zmieniać sposobu implementacji HelloContent
.
Wzór, w którym stan maleje, a liczba zdarzeń rośnie, nazywa się jednokierunkowym przepływem danych. W tym przypadku stan zmienia się z HelloScreen
na HelloContent
, a zdarzenia z HelloContent
na HelloScreen
. Dzięki jednokierunkowemu przepływowi danych możesz odłączyć komponenty wyświetlające stan w interfejsie od części aplikacji, które przechowują i zmieniają stan.
Więcej informacji znajdziesz na stronie Gdzie można umieścić stan.
Przywracanie stanu w sekcji Tworzenie
Interfejs rememberSaveable
działa podobnie do interfejsu remember
, ponieważ zachowuje stan podczas rekompozycji, a także podczas odtwarzania 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 do wyboru kilka opcji.
Parcelize
Najprostszym rozwiązaniem jest dodanie do obiektu adnotacji @Parcelize
. Obiekt staje się podzielny i można go spakować. Na przykład ten kod tworzy typ danych City
, który można podzielić, 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 odpowiedni, możesz użyć funkcji mapSaver
, aby zdefiniować własną regułę konwertowania obiektu na zestaw wartości, które system może zapisać w atrybucie 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ć funkcji listSaver
i 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 edytorze
Prostym przenoszeniem stanu można zarządzać w samych funkcjach składanych. Jeśli jednak ilość stanów, które należy śledzić, rośnie lub jeśli w funkcjach składanych pojawia się logika do wykonania, warto delegować logikę i odpowiedzialnie za stany innym klasom: holderom stanu.
Aby dowiedzieć się więcej, zapoznaj się z dokumentacją dotyczącą przenoszenia stanu w Compose lub ogólniej ze stroną Trzymacze stanu i stan interfejsu użytkownika w przewodniku dotyczącym architektury.
Ponowne wywoływanie obliczeń pamiętania po zmianie kluczy
Interfejs API remember
jest często używany razem z MutableState
:
var name by remember { mutableStateOf("") }
W tym przypadku użycie funkcji remember
powoduje, że wartość MutableState
przetrwa rekompozycje.
Ogólnie funkcja remember
przyjmuje parametr calculation
lambda. Gdy funkcja remember
zostaje uruchomiona po raz pierwszy, wywołuje funkcję lambda calculation
i przechowuje jej wynik. Podczas rekompozycji funkcja remember
zwraca ostatnio zapisaną wartość.
Oprócz buforowania stanu możesz też używać funkcji remember
do przechowywania dowolnego obiektu lub wyniku operacji w kompozycji, której inicjowanie lub obliczanie jest kosztowne. Możesz nie chcieć powtarzać tego obliczenia przy każdym ponownym sformułowaniu.
Przykładem jest tworzenie obiektu ShaderBrush
, który jest kosztowną operacją:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
zapisuje wartość do momentu jej opuszczenia kompozycji. Istnieje jednak sposób na unieważnienie wartości z pamięci podręcznej. Interfejs API remember
przyjmuje też parametr key
lub keys
. Jeśli któryś z tych kluczy ulegnie zmianie, następnym razem, gdy funkcja zostanie ponownie skompilowana, remember
unieważni pamięć podręczną i ponownie wykona obliczenia bloku lambda. Ten mechanizm umożliwia kontrolowanie czasu trwania obiektu w kompozycji. Obliczenie pozostaje ważne do momentu zmiany danych wejściowych, a nie do momentu, gdy zapamiętana wartość opuszcza kompozycję.
Przykłady poniżej pokazują, jak działa ten mechanizm.
W tym fragmencie kodu tworzony jest obiekt ShaderBrush
, który jest używany jako tło do warstwy kompozytowej Box
. remember
przechowuje instancję ShaderBrush
, ponieważ jej odtworzenie jest kosztowne, jak wyjaśniono wcześniej. Funkcja remember
przyjmuje parametr avatarRes
jako key1
, czyli wybrany obraz tła. Jeśli avatarRes
ulegnie zmianie, pędzel zostanie ponownie skomponowany z nowym obrazem i ponownie zastosowany do Box
. Może się to zdarzyć, gdy użytkownik wybierze inny obraz jako tło z selektora.
@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 przeniesiony do zwykłej klasy uchwytu stanu
MyAppState
. Udostępnia on funkcję rememberMyAppState
, która inicjuje wystąpienie klasy za pomocą funkcji remember
. Wyświetlanie takich funkcji w celu utworzenia instancji, która przetrwa ponowne skompilowanie, jest typowym wzorcem w Compose. Funkcja rememberMyAppState
otrzymuje parametr windowSizeClass
, który służy jako parametr key
funkcji remember
. Jeśli ten parametr ulegnie zmianie, aplikacja musi ponownie utworzyć prostą klasę uchwytu stanu z najnowszą wartością. Może się tak zdarzyć, jeśli 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 equals klasy, aby określić, czy klucz się zmienił, i unieważnić zapisaną wartość.
Przechowywanie stanu za pomocą kluczy poza rekompozycją
Interfejs API rememberSaveable
to otoczka dla remember
, która może przechowywać dane w Bundle
. Ten interfejs API pozwala zachować stan nie tylko w przypadku rekompozycji, ale też odtwarzania aktywności i zakończenia procesu przez system.
rememberSaveable
otrzymuje parametry input
w tym samym celu, w jakim remember
otrzymuje parametry keys
. Pamięć podręczna jest unieważniana, gdy zmieni się dowolny z danych wejściowych. Gdy funkcja zostanie ponownie skompilowana, rememberSaveable
ponownie wykona blok funkcji lambda obliczeń.
W tym przykładzie zmienna rememberSaveable
przechowuje wartość zmiennej userTypedQuery
, dopóki zmienna typedQuery
nie ulegnie zmianie:
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.
Próbki
Ćwiczenia z programowania
Filmy
Blogi
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Projektowanie interfejsu tworzenia wiadomości
- Zapisywanie stanu interfejsu w sekcji Utwórz
- Efekty uboczne w edytorze