State i Jetpack Compose

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 z Flow w sposób uwzględniający cykl życia, co pozwala aplikacji oszczędzać jej zasoby. Jest to ostatnia wartość wygenerowana przez funkcję Utwórz State. 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"
}
  • Flow: collectAsState()

    collectAsState jest podobny do collectAsStateWithLifecycle, ponieważ zbiera także wartości z Flow i przekształca go w Utwórz State.

    Jako kodu niezależnego od platformy użyj collectAsState, a nie collectAsStateWithLifecycle, który jest dostępny tylko na Androidzie.

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

  • LiveData: observeAsState()

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

    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"
}

Kotlin

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

Odlotowy

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

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świetlenia
  • onValueChange: (T) -> Unit: zdarzenie, które prosi o zmianę wartości, gdzie T 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 elementu ViewModel.

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