State i Jetpack Compose

Stan w aplikacji to dowolna wartość, która może się z czasem zmieniać. To bardzo szeroka definicja, która obejmuje wszystko, od bazy danych sal po zmienną w klasie.

Wszystkie aplikacje na Androida wyświetlają stan. 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 związane z nim komentarze.
  • Faliste animacje przycisków odtwarzanych po kliknięciu przez użytkownika.
  • Naklejki, które użytkownik może narysować na zdjęciu.

Jetpack Compose pomaga jasno określić, gdzie i jak przechowujesz i wykorzystujesz stan w aplikacji na Androida. Ten przewodnik skupia się na połączeniu między stanem a elementami kompozycyjnymi oraz na interfejsach API, które oferuje Jetpack Compose do łatwiejszej współpracy ze stanem.

Stan i kompozycja

Funkcja tworzenia ma charakter deklaratywny, więc jedynym sposobem jej zaktualizowania jest wywołanie tej samej funkcji kompozycyjnej z nowymi argumentami. Te argumenty reprezentują stan interfejsu. Po każdej aktualizacji stanu następuje zmiana kompozycji. W efekcie elementy takie jak TextField nie są automatycznie aktualizowane tak jak w imperatywnych widokach XML. Funkcja kompozycyjna musi mieć jawną informację o nowym stanie, aby mogła się odpowiednio zaktualizować.

@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 go uruchomisz i spróbujesz wpisać tekst, nic się nie stanie. Wynika to z tego, że TextField się nie aktualizuje, tylko aktualizuje swój parametr value. Wynika to ze sposobu, w jaki działa kompozycja i rekompozycja w funkcji Compose.

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

Stan w elementach kompozycyjnych

Funkcje kompozycyjne mogą używać interfejsu API remember do przechowywania obiektu w pamięci. Wartość obliczona przez funkcję remember jest przechowywana w Kompozycji podczas początkowej kompozycji, a przechowywana 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 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ą ponowne kompozycje wszystkich funkcji kompozycyjnych odczytywanych przez value.

Obiekt MutableState można zadeklarować w funkcji kompozycyjnej 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 do różnych zastosowań stanu. Wybierz ten, który daje najprostszy do odczytania kod w danym elemencie kompozycyjnym.

Składnia delegata by wymaga tych importów:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Możesz użyć zapamiętanej wartości jako parametru dla innych elementów kompozycyjnych lub nawet w postaci logicznej w instrukcjach, aby zmienić wyświetlane elementy kompozycyjne. 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") }
        )
    }
}

Choć funkcja remember pomaga w utrzymaniu stanu w różnych kompozycjach, stan nie jest zachowywany w przypadku zmian konfiguracji. Aby to zrobić, 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 przekazać obiekt wygaszacza niestandardowego.

Inne obsługiwane typy stanu

Funkcja tworzenia nie wymaga używania MutableState<T> do zatrzymywania stanu. Obsługuje inne możliwe do obserwowania typy. Przed odczytaniem innego obserwowalnego typu w funkcji Compose musisz przekonwertować go na format State<T>, aby elementy kompozycyjne mogły automatycznie tworzyć nowe 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 zaczniesz korzystać z integracji, dodaj odpowiednie artefakty zgodnie z tym opisem:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() zbiera wartości z Flow w sposób uwzględniający cykl życia, pozwalając aplikacji oszczędzać jej zasoby. Reprezentuje ostatnią wyemitowaną wartość z funkcji tworzenia wiadomości State. Używaj tego interfejsu API jako zalecanego sposobu gromadzenia przepływów w aplikacjach na Androida.

    Plik build.gradle musi zawierać następującą zależność (powinien to być wersja 2.6.0-beta01 lub nowsza):

Kotlin

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

Odlotowe

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

    Funkcja collectAsState jest podobna do collectAsStateWithLifecycle, ponieważ zbiera też wartości z Flow i przekształca je w Utwórz State.

    Do kodu niezależnego od platformy używaj collectAsState zamiast collectAsStateWithLifecycle, który jest dostępny tylko na Androida.

    W przypadku funkcji collectAsState dodatkowe zależności nie są wymagane, ponieważ jest ono dostępne w interfejsie 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.6.8")
}

Odlotowe

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

Kotlin

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

Odlotowe

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

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.6.8")
}

Odlotowe

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.6.8"
}

Stanowa a bezstanowa

Funkcja kompozycyjna, która używa remember do przechowywania obiektu, tworzy stan wewnętrzny, przez co element kompozycyjny jest stanowy. HelloContent jest przykładem stanowej funkcji kompozycyjnej, ponieważ przechowuje i zmienia swój stan name wewnętrznie. Jest to przydatne w sytuacjach, gdy wywołujący stan 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ą rzadziej używane do wielokrotnego użytku i trudniejsze są do przetestowania.

Funkcja bezstanowa kompozycyjna to funkcja kompozycyjna, która nie zachowuje żadnego stanu. Prostym sposobem na osiągnięcie stanu bezstanowego jest użycie funkcji podnoszenia stanu.

Gdy tworzysz elementy kompozycyjne wielokrotnego użytku, często chcesz udostępnić zarówno stanową, jak i bezstanową wersję tego samego elementu kompozycyjnego. Wersja stanowa jest wygodna dla rozmówców, którym nie zależy na stanie. Wersja bezstanowa jest niezbędna w przypadku aplikacji wywołujących, które muszą kontrolować lub podnosić stan.

Podnośnik stanu

Podnoszenie stanu w funkcji tworzenia wiadomości to wzorzec przenoszenia stanu do elementu wywołującego funkcję kompozycyjną, co powoduje przekształcenie elementu kompozycyjnego do stanu bezstanowego. Ogólnym wzorcem przenoszenia stanów w Jetpack Compose jest zastąpienie zmiennej stanu 2 parametrami:

  • value: T: bieżąca wartość do wyświetlenia
  • onValueChange: (T) -> Unit: zdarzenie żądające zmiany wartości, gdzie T to proponowana nowa wartość.

Nie dotyczy Cię jednak ograniczenie do onValueChange. Jeśli dla funkcji kompozycyjnej potrzebne są bardziej szczegółowe zdarzenia, zdefiniuj je za pomocą parametrów lambda.

Stan, który jest przenoszony w ten sposób, ma kilka ważnych właściwości:

  • Jedno źródło danych: przesuwając stan zamiast go duplikować, mamy pewność, ż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. Mają charakter wewnętrzny.
  • Możliwość udostępniania: stan przeciągania można udostępnić wielu elementom kompozycyjnym. Jeśli chcesz przeczytać utwór name w innym elemencie kompozycyjnym, możesz to zrobić, podnosząc go.
  • 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. Można na przykład przenieść name do ViewModel.

W tym przykładzie wyodrębniasz name i onValueChange z HelloContent i przenosisz je w górę do kompozycji 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") })
    }
}

Dzięki podniesieniu stanu z HelloContent łatwiej jest wyciągać wnioski na temat elementu kompozycyjnego, używać go w różnych sytuacjach oraz testować. Element HelloContent jest oddzielony od sposobu przechowywania stanu. Usunięcie powiązania oznacza, że jeśli zmodyfikujesz lub zastąpisz HelloScreen, nie musisz zmieniać sposobu implementacji obiektu HelloContent.

Wzorzec, w którym zachodzi spadek i zdarzenia spadające w górę, nazywamy jednokierunkowym przepływem danych. W tym przypadku stan maleje z HelloScreen do HelloContent, a liczba zdarzeń wzrasta z HelloContent do 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 przenieść stan.

Przywracanie stanu w funkcji tworzenia wiadomości

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 zapisanych stanów instancji. Dzieje się tak na przykład po obróceniu ekranu.

Sposoby zapisywania stanu

Wszystkie typy danych dodane do usługi 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 stanie się częścią pakietu i można go połączyć w pakiet. Na przykład ten kod tworzy typ danych City z możliwością parowania 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 mapy

Jeśli z jakiegoś powodu @Parcelize jest nieodpowiedni, możesz użyć mapSaver, by zdefiniować własną regułę konwertowania obiektu na zbiór wartości, które system może zapisać w obiekcie 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 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"))
    }
}

Posiadacze stanów w usłudze Compose

Prostym przenoszeniem stanów można zarządzać bezpośrednio w funkcjach kompozycyjnych. Jeśli jednak pojawi się ilość stanu umożliwiającego śledzenie wzrostu lub pojawi się logika wykonywania funkcji kompozycyjnych, warto przekazać obowiązki logiczne i stanowe innym klasom: właścicielom stanów.

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.

Ponownie wywołuj zapamiętywanie obliczeń 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 działa po zmianie kompozycji.

Ogólnie funkcja remember przyjmuje parametr lambda calculation. Po pierwszym uruchomieniu funkcja remember wywołuje funkcję lambda calculation i zapisuje jej wynik. Podczas zmiany kompozycji funkcja remember zwraca ostatnio zapisaną wartość.

Oprócz stanu buforowania za pomocą funkcji remember możesz też przechowywać dowolny obiekt lub wynik operacji w kompozycji, której zainicjowanie lub obliczenie jest kosztowne. Możesz nie chcieć powtarzać tych obliczeń przy każdej zmianie kompozycji. 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 unieważnienia wartości zapisanej w pamięci podręcznej. Interfejs remember API również przyjmuje parametr key lub keys. Jeśli którykolwiek z tych kluczy się zmieni, przy następnym tworzeniu funkcji ponownego tworzenia remember unieważni pamięć podręczną i ponownie wykonuje blok lambda obliczeń. Ten mechanizm daje Ci kontrolę nad czasem trwania obiektu w kompozycji. Obliczenie obowiązuje 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 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. remember przyjmuje avatarRes parametr key1, który jest wybranym obrazem tła. Jeśli wartość avatarRes się zmieni, pędzel ponownie komponuje się z nowym obrazem i ponownie zastosuje format 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 kodu stan jest przenoszony do klasy zwykłej właściciela stanu MyAppState. Udostępnia funkcję rememberMyAppState do zainicjowania instancji klasy za pomocą remember. Udostępnianie takich funkcji w celu utworzenia instancji, która przetrwa przekompozycje, to częsty wzorzec w funkcji Compose. Funkcja rememberMyAppState otrzymuje parametr windowSizeClass, który służy jako parametr key dla remember. Jeśli ten parametr się zmieni, aplikacja będzie musiała odtworzyć klasę obiektu plain State holder z najnowszą wartością. Może się tak zdarzyć, np. gdy 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 korzysta z implementacji klasy jest równe, by zdecydować, czy klucz zmienił i unieważnił zapisaną wartość.

Zapisuj stan z kluczami poza możliwością zmiany kompozycji

Interfejs API rememberSaveable jest otoką wokół remember, która może przechowywać dane w Bundle. Ten interfejs API umożliwia stanowi przetrwanie nie tylko ponownej kompozycji, lecz także rozrywki czy zainicjowanej przez system śmierci procesu. Funkcja rememberSaveable otrzymuje parametry input w tym samym celu co remember otrzymuje keys. Pamięć podręczna zostaje unieważniona w przypadku zmiany danych wejściowych Przy następnym tworzeniu bloków lambda funkcja rememberSaveable ponownie wykonuje blok lambda.

W tym przykładzie rememberSaveable przechowuje userTypedQuery do momentu wprowadzenia zmian typedQuery:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

Więcej informacji

Aby dowiedzieć się więcej o stanie i Jetpack Compose, zapoznaj się z dodatkowymi materiałami poniżej.

Próbki

Ćwiczenia z programowania

Filmy

Blogi