State i Jetpack Compose

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ę po kliknięciu przez użytkownika.
  • Naklejki, które użytkownik może narysować na obrazie.

Jetpack Compose pomaga określić, gdzie i jak przechowywać stan oraz jak z niego korzystać 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 elementy takie jak TextField nie są automatycznie aktualizowane tak jak w imperatywnych widokach 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 – robi to, gdy zmieni się parametr value. Wynika to z tego, jak działają funkcje tworzenia i ponownego tworzenia w Compose.

Więcej informacji o początkowej kompozycji i rekompozycji znajdziesz w artykule Thinking in Compose.

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 obserwowalny typ MutableState<T>, który jest obserwacyjnym typem zintegrowanym ze środowiskiem wykonawczym tworzenia.

interface MutableState<T> : State<T> {
    override var value: T
}

Wszelkie zmiany w funkcji 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 skróty składniowe do różnych zastosowań stanu. 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 dla innych komponentów lub nawet jako logiki w oświadczeniach, aby zmienić wyświetlane komponenty. Jeśli na przykład nie chcesz wyświetlać powitania, jeśli jego 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 w przypadku rekompozycji, ale nie zachowuje stanu w przypadku zmian konfiguracji. W tym celu musisz użyć rememberSaveable. rememberSaveable automatycznie zapisuje wszystkie wartości, które można zapisać w pliku Bundle. W przypadku innych wartości możesz podać obiekt niestandardowego obiektu zapisu.

Inne obsługiwane typy stanu

Funkcja tworzenia nie wymaga używania MutableState<T> do przechowywania stanu. Obsługuje inne możliwe do obserwowania 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.

Funkcja tworzenia wiadomości zawiera funkcje tworzące element State<T> na podstawie typowych dostrzegalnych typów 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 z Flow w sposób uwzględniający cykl życia, co pozwala aplikacji oszczędzać zasoby. Reprezentuje ona ostatnią wyemitowaną wartość z funkcji Compose State. Zalecamy korzystanie z tego interfejsu API do zbierania danych w aplikacjach na Androida.

    W pliku build.gradle (powinien być w wersji 2.6.0-beta01 lub nowszej) wymagana jest ta zależność:

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}

Odlotowe

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • Flow: collectAsState()

    Funkcja collectAsState jest podobna do funkcji collectAsStateWithLifecycle, ponieważ również zbiera wartości z elementu Flow i przekształca je w element Compose State.

    Zamiast collectAsStateWithLifecycle, który jest przeznaczony tylko na Androida, używaj kodu niezależnego od platformy (collectAsState).

    Dodatkowe zależności nie są wymagane w przypadku collectAsState, ponieważ jest on dostępny w compose-runtime.

  • LiveData: observeAsState()

    observeAsState() zaczyna obserwować ten element (LiveData) i przedstawiać jego wartości za pomocą State.

    W pliku build.gradle wymagana jest ta zależność:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}

Odlotowe

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}

Odlotowe

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}

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 stanowej funkcji kompozycyjnej, ponieważ przechowuje i zmienia swój stan name wewnętrznie. Może to być przydatne w sytuacjach, gdy dzwoniący nie musi kontrolować stanu i może go używać bez konieczności samodzielnego zarządzania nim. Jednak elementy kompozycyjne ze stanem wewnętrznym są rzadziej używane do wielokrotnego użytku i trudniejsze są do przetestowania.

Bezstanowa usługa składana to usługa składana, która 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ć.

Podnośnik 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świetlenia
  • onValueChange: (T) -> Unit: zdarzenie, które prosi o zmianę wartości, gdzie T 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. Pomoże to uniknąć błędów.
  • Zamknięta: tylko stanowe elementy kompozycyjne mogą zmieniać swój stan. Jest to całkowicie wewnętrzna zmiana.
  • Możliwość udostępniania: stan przeciągania można udostępnić wielu elementom kompozycyjnym. Jeśli chcesz odczytać name w innej składanej, możesz to zrobić dzięki funkcji podnoszenia.
  • Intercepable: wywołujące bezstanowe funkcje kompozycyjne mogą ignorować lub modyfikować zdarzenia przed zmianą stanu.
  • Rozłączone: stan bezstanowych elementów kompozycyjnych może być przechowywany w dowolnym miejscu. Teraz można na przykład przenieść name do ViewModel.

W tym przykładzie z komponentu HelloContent wyodrębniasz elementy nameonValueChange, a potem przenosisz je wyżej w drzewie do 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ć. Element HelloContent jest oddzielony 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. Korzystając z jednokierunkowego przepływu danych, możesz oddzielić obiekty 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 można umieścić stan.

Przywracanie stanu w sekcji Tworzenie

Interfejs API rememberSaveable działa podobnie jak remember, ponieważ zapisuje stan w różnych rekompozycjach, a także w czasie aktywności lub odtworzenia procesu z użyciem mechanizmu zapisanego stanu instancji. Dzieje się tak na przykład po obróceniu ekranu.

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 możliwości.

Parcelyzacja

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ć parametru 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"))
    }
}

Zmienne stanu w edytorze

Prostym przenoszeniem stanu można zarządzać bezpośrednio w funkcjach kompozycyjnych. 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ą na temat przenoszenia stanów w narzędziu Compose lub, bardziej ogólnie, na stronie Właściciele stanów i stany UI w przewodniku po architekturze.

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 rememberzostaje 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 może być utworzenie tego obiektu ShaderBrush, co jest kosztowną operacją:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

Funkcja remember przechowuje wartość, dopóki nie opuści 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 obowiązuje do momentu zmiany danych wejściowych, a nie do momentu, gdy zapamiętana wartość opuści Kompozycję.

Przykłady poniżej pokazują, jak działa ten mechanizm.

W tym fragmencie kodu ShaderBrush jest tworzony i używany jako tło w funkcji kompozycyjnej Box. remember przechowuje instancję ShaderBrush, ponieważ jej odtworzenie jest drogie, jak wyjaśniliśmy 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 przenoszony do klasy zwykłej właściciela stanu MyAppState. Udostępnia funkcję rememberMyAppState do zainicjowania instancji klasy za pomocą 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 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
) { /* ... */ }

Funkcja tworzenia używa implementacji klasy jest równe, by zdecydować, czy klucz 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