Stan aplikacji to dowolna wartość, która może się zmieniać z upływem czasu. Jest to bardzo szeroka definicja i obejmuje wszystko – od bazy danych sal po zmienne w klasie.
Wszystkie aplikacje na Androida wyświetlają stan użytkownikowi. Oto kilka przykładów stanu w aplikacjach na Androida:
- Pasek powiadomień, który wyświetla się, gdy nie można nawiązać połączenia sieciowego.
- Post na blogu i powiązane z nim komentarze.
- Faliste animacje na przyciskach uruchamianych po kliknięciu ich przez użytkownika.
- Naklejki, które użytkownik może narysować na obrazie.
Jetpack Compose pomaga dokładnie określić, gdzie i w jaki sposób przechowujesz i używaj stanu w aplikacji na Androida. Ten przewodnik skupia się na połączeniu stanu z obiektami kompozycyjnymi oraz na interfejsach API, które Jetpack Compose oferuje do łatwiejszej pracy z różnymi stanami.
Stan i kompozycja
Tworzenie jest deklaratywne, więc jedynym sposobem jego aktualizacji jest wywołanie tego samego funkcji kompozycyjnej z nowymi argumentami. Te argumenty reprezentują stan interfejsu. Po każdej zmianie stanu następuje zmiana kompozycji. W rezultacie elementy takie jak TextField
nie są automatycznie aktualizowane tak jak w przypadku imperatywnych widoków danych XML. Aby element kompozycyjny odpowiednio się zaktualizował, musi zostać wyraźnie wskazany nowy stan.
@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 go i spróbujesz wpisać tekst, zobaczysz, że nic się nie dzieje. Dzieje się tak, ponieważ TextField
nie aktualizuje się samodzielnie, lecz po zmianie parametru value
. Wynika to ze sposobu działania
kompozycji i zmiany kompozycji.
Więcej informacji o początkowej kompozycji i zmianie kompozycji znajdziesz na stronie Myślenie w trakcie tworzenia.
Stan w obiektach kompozycyjnych
Funkcje kompozycyjne mogą używać interfejsu API remember
do zapisywania obiektu w pamięci. Wartość obliczona przez remember
jest przechowywana w kompozycji podczas początkowej kompozycji, a zapisana wartość jest zwracana podczas ponownego komponowania.
remember
może służyć do przechowywania zarówno obiektów zmiennych, jak i stałych.
mutableStateOf
tworzy możliwy do obserwowania typ MutableState<T>
, który jest zintegrowany ze środowiskiem wykonawczym tworzenia.
interface MutableState<T> : State<T> {
override var value: T
}
Wszelkie zmiany w value
planują rekomponowanie 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 są dostarczane jako cukier składniowy na potrzeby różnych zastosowań stanu. Wybierz ten, który generuje najłatwiejszy do odczytania kod w tworzonym elemencie kompozycyjnym.
Składnia delegata by
wymaga importowania tych danych:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Zapamiętanej wartości możesz używać jako parametru innych funkcji kompozycyjnych, a nawet jako logiki 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 wszystkich zmian w konfiguracji, ale nie jest zachowywany po wszystkich zmianach w konfiguracji. Aby to zrobić, musisz użyć właściwości rememberSaveable
. rememberSaveable
automatycznie zapisuje wartość,
która można zapisać w elemencie Bundle
. W przypadku innych wartości możesz przekazać niestandardowy obiekt oszczędzania.
Inne obsługiwane typy stanu
Tworzenie nie wymaga użycia metody MutableState<T>
do zatrzymywania stanu. Obsługuje ona inne obserwowalne typy. Zanim odczytasz inny obserwowalny typ w interfejsie Compose, musisz go przekonwertować na typ State<T>
, aby funkcja kompozycyjna mogła automatycznie utworzyć się ponownie po zmianie stanu.
Komponowanie obejmuje funkcje umożliwiające utworzenie State<T>
na podstawie typowych dostrzegalnych typów używanych w aplikacjach na Androida. Przed rozpoczęciem integracji dodaj odpowiednie artefakty zgodnie z tymi instrukcjami:
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
zbiera wartości zFlow
w sposób uwzględniający cykl życia, co pozwala aplikacji oszczędzać jej zasoby. Jest to ostatnia wartość wygenerowana przez funkcję UtwórzState
. Używaj tego interfejsu API jako zalecanego sposobu gromadzenia przepływów w aplikacjach na Androida.Plik
build.gradle
wymaga tej zależności (powinien mieć wersję 2.6.0-beta01 lub nowszą):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
}
Odlotowy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
}
-
collectAsState
jest podobny docollectAsStateWithLifecycle
, ponieważ zbiera także wartości zFlow
i przekształca go w UtwórzState
.Jako kodu niezależnego od platformy użyj
collectAsState
, a niecollectAsStateWithLifecycle
, który jest dostępny tylko na Androidzie.Dodatkowe zależności nie są wymagane w przypadku
collectAsState
, ponieważ jest ona dostępna wcompose-runtime
. -
observeAsState()
zaczyna obserwować ten element (LiveData
) i przedstawia jego wartości za pomocą funkcjiState
.Plik
build.gradle
wymaga tej zależności:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.6.1")
}
Odlotowy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.6.1"
}
-
subscribeAsState()
to funkcje rozszerzenia, które przekształcają strumienie reaktywne w RxJava2 (np.Single
,Observable
,Completable
) w tworzenieState
.Plik
build.gradle
wymaga tej zależności:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.6.1")
}
Odlotowy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.6.1"
}
-
subscribeAsState()
to funkcje rozszerzenia, które przekształcają strumienie reaktywne w RxJava3 (np.Single
,Observable
,Completable
) w tworzenieState
.Plik
build.gradle
wymaga tej zależności:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.6.1")
}
Odlotowy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.6.1"
}
Stanowa lub bezstanowa
Funkcja kompozycyjna, która do przechowywania obiektu używa polecenia remember
, tworzy stan wewnętrzny, przez co funkcja kompozycyjna jest stanowa. HelloContent
jest przykładem stanu kompozycyjnego, ponieważ przechowuje i modyfikuje wewnętrznie swój stan name
. Może to być przydatne w sytuacjach, gdy element wywołujący nie musi kontrolować stanu i może go używać bez konieczności samodzielnego zarządzania stanem. Jednak obiekty kompozycyjne w stanie wewnętrznym są mniej nadające się do wielokrotnego użytku i trudniejsze do testowania.
Funkcja kompozycyjna bezstanowa to element kompozycyjny, który nie zawiera żadnego stanu. Łatwym sposobem na osiągnięcie bezstanowych celów jest użycie metody przenoszenia stanów.
Przy tworzeniu funkcji kompozycyjnych wielokrotnego użytku często chcesz pokazać zarówno stanową, jak i bezstanową wersję tego samego elementu kompozycyjnego. Wersja stanowa jest wygodna w przypadku rozmówców, którym nie zależy na stanie, a wersja bezstanowa jest niezbędna w przypadku rozmówców, którzy muszą sterować lub przenosić stan.
Podnośnik wojewódzki
Przenoszenie stanu w Compose to wzorzec przenoszenia stanu do elementu wywołującego kompozycję w celu przekształcenia funkcji kompozycyjnej w bezstanową. Ogólny wzorzec przenoszenia stanu w Jetpack Compose to zastępowanie zmiennej stanu dwoma parametrami:
value: T
: bieżąca wartość do wyświetleniaonValueChange: (T) -> Unit
: zdarzenie, które prosi o zmianę wartości, gdzieT
to proponowana nowa wartość.
Pamiętaj jednak, że nie musisz ograniczać się do onValueChange
. Jeśli do funkcji kompozycyjnej pasuje bardziej szczegółowe zdarzenia, zdefiniuj je za pomocą funkcji lambda.
Przenoszony w ten sposób stan ma kilka ważnych właściwości:
- Jedno źródło wiarygodnych danych: dzięki przenoszeniu stanów zamiast ich duplikowania, zapewniamy, że istnieje tylko jedno źródło wiarygodnych danych. Pomaga to uniknąć błędów.
- Publikowany: tylko stanowe elementy kompozycyjne mogą zmieniać swój stan. Ma charakter całkowicie wewnętrzny.
- Udostępnianie: stan „Podniesiony” można udostępniać wielu elementom kompozycyjnym. Jeśli chcesz odczytać
name
w innym elemencie kompozycyjnym, możesz to zrobić przez podniesienie. - Interceptable: elementy wywołujące bezstanowe funkcje kompozycyjne mogą ignorować lub modyfikować zdarzenia przed zmianą stanu.
- Rozłączone: stan bezstanowych funkcji kompozycyjnych może być przechowywany w dowolnym miejscu. Można na przykład przenieść element
name
do elementuViewModel
.
W tym przykładzie wyodrębniasz name
oraz onValueChange
z tabeli HelloContent
i przenosisz je w górę do funkcji kompozycyjnej HelloScreen
o nazwie 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") }) } }
Przenosząc stan z elementu HelloContent
, ułatwiasz radzenie sobie z komponentem, wykorzystywanie go w różnych sytuacjach i testowanie. Element HelloContent
nie jest połączony ze sposobem przechowywania informacji o jego stanie. Rozłączenie oznacza, że jeśli zmodyfikujesz lub zastąpisz dyrektywę HelloScreen
, nie musisz zmieniać sposobu implementacji HelloContent
.
Wzorzec malejący stanu i rosnącego stanu zdarzeń jest nazywany jednokierunkowym przepływem danych. W tym przypadku stan zmniejszy się z HelloScreen
do HelloContent
, a liczba zdarzeń wzrośnie z HelloContent
do HelloScreen
. Śledząc jednokierunkowy przepływ danych, możesz oddzielić elementy kompozycyjne wyświetlające stan w interfejsie od tych części aplikacji, które przechowują i zmieniają stan.
Więcej informacji znajdziesz na stronie Gdzie przenieść stan.
Przywracam stan w widoku tworzenia
Interfejs API rememberSaveable
działa podobnie do remember
, ponieważ zachowuje stan zarówno podczas rekompozycji, jak i w czasie aktywności lub odtwarzania procesów z wykorzystaniem zapisanego mechanizmu stanu instancji. Dzieje się tak np., gdy obrócisz ekran.
Sposoby przechowywania informacji o stanie
Wszystkie typy danych dodawane do Bundle
są zapisywane automatycznie. Jeśli chcesz zapisać coś, czego nie można dodać do Bundle
, masz kilka możliwości.
Działaj
Najprostszym rozwiązaniem jest dodanie do obiektu adnotacji @Parcelize
. Obiekt staje się papierowy i można go połączyć w pakiet. Na przykład ten kod tworzy możliwy do przekształcenia typ danych City
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")) } }
Zapisywanie map
Jeśli z jakiegoś powodu funkcja @Parcelize
nie jest odpowiednia, możesz użyć funkcji mapSaver
, aby zdefiniować własną regułę przekształcania obiektu w 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")) } }
Zapisywanie listy
Aby uniknąć konieczności definiowania kluczy na potrzeby mapy, możesz też użyć parametru listSaver
i używać 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")) } }
Właściciele stanów w tworzeniu wiadomości
Prostym przenoszeniem stanu można zarządzać w samych funkcjach kompozycyjnych. Jeśli jednak pojawi się zakres obowiązków logicznych i stanowych do śledzenia wzrostów lub pojawienie się logiki wykonywania funkcji kompozycyjnych, warto przekazać obowiązki logiczne i stanowe innym klasom, takim jak stoi państwowi.
Więcej informacji znajdziesz w dokumentacji przenoszenia stanu w Compose lub (przede wszystkim) na stronie Właściciele stanów i stan interfejsu użytkownika w przewodniku po architekturze.
Ponownie aktywuj zapamiętywanie obliczeń po zmianie kluczy
Interfejs API remember
jest często używany razem z MutableState
:
var name by remember { mutableStateOf("") }
W tym przykładzie użycie funkcji remember
sprawia, że wartość MutableState
zachowuje zgodność ze zmianami kompozycji.
Zwykle remember
przyjmuje parametr lambda calculation
. Przy pierwszym uruchomieniu remember
wywołuje lambda calculation
i zapisuje swój wynik. Podczas zmiany kompozycji remember
zwraca ostatnio zapisaną wartość.
Oprócz stanu buforowania możesz też używać remember
do przechowywania dowolnego obiektu lub wyniku operacji w kompozycji, której inicjowanie lub obliczenia jest kosztowne. Takie obliczenia nie muszą być powtarzane przy każdej zmianie kompozycji.
Przykładem może być utworzenie obiektu ShaderBrush
, który jest kosztowną operacją:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
przechowuje wartość, dopóki nie opuści kompozycji. Istnieje jednak sposób, aby unieważnić wartość z 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 tworzeniu funkcji przez funkcję remember
unieważni pamięć podręczną i ponownie wykonuje blok lambda obliczeniowy. Ten mechanizm zapewnia kontrolę nad czasem życia obiektu
w kompozycji. Obliczenie obowiązuje do czasu 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.
Ten fragment kodu pokazuje obiekt ShaderBrush
, który jest używany jako wyrenderowanie tła elementu kompozycyjnego Box
. remember
przechowuje instancję ShaderBrush
, ponieważ jak wyjaśniliśmy wcześniej, jej odtworzenie jest kosztowne. remember
przyjmuje avatarRes
jako parametr key1
, który jest wybranym obrazem tła. Jeśli avatarRes
się zmieni, pędzel utworzy kompozycję z nowym obrazem i ponownie zostanie zastosowany do obrazu Box
. Może się tak zdarzyć, gdy użytkownik wybierze z selektora inny obraz, który ma być tłem.
@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 stan jest przenoszony do klasy posiadacza prostego stanu
MyAppState
. Ujawnia on funkcję rememberMyAppState
do zainicjowania instancji klasy za pomocą remember
. Ujawnianie takich funkcji w celu utworzenia instancji, która przetrwa zmiany kompozycji, jest częstym wzorcem używania w komponencie. Funkcja rememberMyAppState
otrzymuje wartość windowSizeClass
, która służy jako parametr key
dla właściwości remember
. Jeśli ten parametr ulegnie zmianie, aplikacja musi odtworzyć klasę prostego stanu z najnowszą wartością. Może się tak zdarzyć, jeśli 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 ) { /* ... */ }
Tworzenie za pomocą implementacji klasy równa się pozwala określić, czy klucz uległ zmianie i unieważnił zapisaną wartość.
Zapisuj stan z kluczami wykraczającymi poza zmianę kompozycji
Interfejs API rememberSaveable
to otoka remember
, która może przechowywać dane w obiekcie Bundle
. Ten interfejs API pozwala na przetrwanie nie tylko zmiany kompozycji, ale także odtwarzania aktywności i zakończenia procesu inicjowanego przez system.
rememberSaveable
otrzymuje parametry input
w tym samym celu, co remember
otrzymuje keys
. Pamięć podręczna jest unieważniona po zmianie jakichkolwiek danych wejściowych. Podczas ponownego tworzenia funkcji rememberSaveable
ponownie wykonuje blok lambda obliczeń.
W tym przykładzie rememberSaveable
przechowuje userTypedQuery
do chwili zmiany wartości 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.
Próbki
Ćwiczenia z programowania
Filmy
Blogi
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Tworzenie interfejsu użytkownika tworzenia wiadomości
- Zapisywanie stanu interfejsu użytkownika w momencie tworzenia
- Efekty uboczne w oknie tworzenia wiadomości