Cykl życia elementów kompozycyjnych

Na tej stronie dowiesz się więcej o cyklu życia funkcji kompozycyjnej oraz o tym, jak Compose decyduje, czy funkcja kompozycyjna wymaga rekompozycji.

Omówienie cyklu życia

Jak wspomnieliśmy w dokumentacji Zarządzanie stanem, kompozycja opisuje interfejs aplikacji i jest tworzona przez uruchamianie funkcji typu „composable”. Kompozycja to struktura drzewiasta funkcji typu „composable”, które opisują interfejs.

Gdy Jetpack Compose po raz pierwszy uruchamia funkcje typu „composable” podczas początkowej kompozycji, śledzi funkcje typu „composable”, które wywołujesz, aby opisać interfejs w kompozycji. Następnie, gdy stan aplikacji się zmieni, Jetpack Compose zaplanuje rekompozycję. Ponowne utworzenie to sytuacja, w której Jetpack Compose ponownie wykonuje funkcje typu „composable”, które mogły się zmienić w odpowiedzi na zmiany stanu, a następnie aktualizuje kompozycję, aby odzwierciedlić te zmiany.

Kompozycję można utworzyć tylko za pomocą początkowej kompozycji, a zaktualizować za pomocą ponownego utworzenia. Jedynym sposobem na zmodyfikowanie kompozycji jest ponowne utworzenie.

Diagram przedstawiający cykl życia komponentu
Rysunek 1. Cykl życia funkcji typu „composable” w kompozycji. Funkcja typu „composable” wchodzi do kompozycji, jest ponownie tworzona 0 lub więcej razy i opuszcza kompozycję.

Rekompozycja jest zwykle wywoływana przez zmianę obiektu State<T>. Compose śledzi te zmiany i uruchamia wszystkie funkcje typu „composable” w kompozycji, które odczytują ten konkretny State<T>, oraz wszystkie funkcje typu „composable”, które wywołują i których nie można pominąć.

Jeśli funkcja typu „composable” jest wywoływana wiele razy, w kompozycji umieszczanych jest wiele jej instancji. Każde wywołanie ma własny cykl życia w kompozycji.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

Diagram przedstawiający hierarchiczne ułożenie elementów w poprzednim fragmencie kodu
Rysunek 2. Reprezentacja MyComposable w kompozycji. Jeśli funkcja typu „composable” jest wywoływana wiele razy, w kompozycji umieszczanych jest wiele jej instancji. Element o innym kolorze oznacza, że jest to osobna instancja.

Anatomia funkcji typu „composable” w kompozycji

Instancja funkcji typu „composable” w kompozycji jest identyfikowana przez miejsce wywołania. Kompilator Compose traktuje każde miejsce wywołania jako odrębne. Wywoływanie funkcji typu „composable” z wielu miejsc wywołania spowoduje utworzenie w kompozycji wielu instancji tej funkcji.

Jeśli podczas ponownego utworzenia funkcja typu „composable” wywołuje inne funkcje typu „composable” niż podczas poprzedniej kompozycji, Compose określi, które funkcje typu „composable” zostały wywołane, a które nie, a w przypadku funkcji typu „composable”, które zostały wywołane w obu kompozycjach, pominie ponowne utworzenie, jeśli ich dane wejściowe się nie zmieniły.

Zachowanie tożsamości jest kluczowe, aby powiązać efekty uboczne z funkcją typu „composable”, tak aby mogły się one zakończyć pomyślnie, a nie restartować przy każdym ponownym utworzeniu.

Przyjrzyj się temu przykładowi:

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }

W powyższym fragmencie kodu funkcja LoginScreen warunkowo wywołuje funkcję typu „composable” LoginError i zawsze wywołuje funkcję typu „composable” LoginInput. Każde wywołanie ma unikalne miejsce wywołania i pozycję źródłową, których kompilator używa do jego jednoznacznego zidentyfikowania.

Diagram pokazujący, jak poprzedni kod zostanie przekształcony, jeśli flaga showError zostanie zmieniona na true. Kompozycja LoginError jest dodawana, ale inne kompozycje nie są ponownie komponowane.
Rysunek 3. Reprezentacja LoginScreen w kompozycji, gdy stan się zmienia i następuje ponowne utworzenie. Ten sam kolor oznacza, że funkcja nie została ponownie utworzona.

Mimo że funkcja LoginInput była najpierw wywoływana jako pierwsza, a potem jako druga, jej instancja LoginInput zostanie zachowana podczas ponownego utworzenia. Dodatkowo, ponieważ funkcja LoginInput nie ma żadnych parametrów, które zmieniły się podczas rekompozycji, wywołanie funkcji LoginInput zostanie pominięte przez Compose.

Dodawanie dodatkowych informacji, które pomagają w inteligentnym ponownym tworzeniu

Wywołanie funkcji typu „composable” wiele razy spowoduje dodanie jej do kompozycji również wiele razy. Gdy funkcja typu „composable” jest wywoływana wiele razy z tego samego miejsca wywołania, Compose nie ma informacji, które pozwoliłyby jednoznacznie zidentyfikować każde wywołanie tej funkcji. Dlatego oprócz miejsca wywołania używana jest kolejność wykonywania, aby zachować odrębność instancji. To zachowanie czasami wystarcza, ale w niektórych przypadkach może powodować niepożądane działanie.

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

W powyższym przykładzie Compose używa kolejności wykonywania oprócz miejsca wywołania, aby zachować odrębność instancji w kompozycji. Jeśli nowy movie zostanie dodany na dół listy, Compose może ponownie użyć instancji, które są już w kompozycji, ponieważ ich położenie na liście się nie zmieniło, a więc dane wejściowe movie są takie same w przypadku tych instancji.

Diagram pokazujący, jak poprzedni kod jest ponownie komponowany, gdy na dole listy zostanie dodany nowy element. Pozostałe elementy na liście nie zmieniły pozycji i nie zostały ponownie skomponowane.
Rysunek 4. Reprezentacja MoviesScreen w kompozycji, gdy nowy element zostanie dodany na dół listy. Funkcje typu „composable” MovieOverview w kompozycji można ponownie wykorzystać. Ten sam kolor w MovieOverview oznacza, że funkcja typu „composable” nie została ponownie utworzona.

Jeśli jednak lista movies zmieni się przez dodanie elementu na górę lub środek listy, usunięcie lub zmianę kolejności elementów, spowoduje to ponowne utworzenie wszystkich wywołań MovieOverview, których parametr wejściowy zmienił pozycję na liście. Jest to niezwykle ważne, jeśli np. funkcja MovieOverview pobiera obraz filmu za pomocą efektu ubocznego. Jeśli rekompozycja nastąpi podczas wykonywania efektu, zostanie on anulowany i uruchomi się ponownie.

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

Diagram pokazujący, jak poprzedni kod jest ponownie komponowany, gdy na górze listy zostanie dodany nowy element. Każdy inny element na liście zmienia pozycję i musi zostać ponownie skomponowany.
Rysunek 5. Reprezentacja MoviesScreen w kompozycji, gdy nowy element zostanie dodany do listy. Funkcji typu „composable” MovieOverview nie można ponownie użyć, a wszystkie efekty uboczne zostaną zrestartowane. Inny kolor w MovieOverview oznacza, że funkcja typu „composable” została ponownie utworzona.

Najlepiej byłoby, gdyby tożsamość instancji MovieOverview była powiązana z tożsamością przekazywanego do niej movie. Jeśli zmienimy kolejność listy filmów, najlepiej byłoby, gdybyśmy w podobny sposób zmienili kolejność instancji w drzewie kompozycji, zamiast ponownie tworzyć każdą funkcję typu „composable” MovieOverview z inną instancją filmu. Compose umożliwia określenie, jakich wartości chcesz używać do identyfikowania danej części drzewa: key funkcji typu „composable”.

Jeśli otoczysz blok kodu wywołaniem funkcji typu „composable” key z przekazanymi co najmniej 2 wartościami, te wartości zostaną połączone i użyte do zidentyfikowania tej instancji w kompozycji. Wartość key nie musi być globalnie unikalna, ale musi być unikalna wśród wywołań funkcji typu „composable” w miejscu wywołania. W tym przykładzie każdy movie musi mieć key, który jest unikalny wśród movies. Nie ma problemu, jeśli ten key jest współdzielony z inną funkcją typu „composable” w innej części aplikacji.

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

Dzięki temu nawet jeśli elementy na liście się zmienią, Compose rozpoznaje poszczególne wywołania MovieOverview i może ich ponownie użyć.

Diagram pokazujący, jak poprzedni kod jest ponownie komponowany, gdy na górze listy zostanie dodany nowy element. Ponieważ elementy listy są identyfikowane za pomocą kluczy, Compose wie, że nie trzeba ich ponownie komponować, mimo że ich pozycje się zmieniły.
Rysunek 6. Reprezentacja MoviesScreen w kompozycji, gdy nowy element zostanie dodany do listy. Ponieważ funkcje typu „composable” MovieOverview mają unikalne klucze, Compose rozpoznaje, które instancje MovieOverview się nie zmieniły, i może ich ponownie użyć. Ich efekty uboczne będą nadal wykonywane.

Niektóre funkcje typu „composable” mają wbudowaną obsługę funkcji typu „composable” key. Na przykład LazyColumn umożliwia określenie niestandardowego key w DSL items.

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

Pomijanie, jeśli dane wejściowe się nie zmieniły

Podczas ponownego utworzenia wykonanie niektórych kwalifikujących się funkcji typu „composable” można całkowicie pominąć, jeśli ich dane wejściowe nie zmieniły się od czasu poprzedniej kompozycji.

Funkcja typu „composable” kwalifikuje się do pominięcia chyba że:

  • funkcja ma typ zwracany inny niż Unit.
  • funkcja jest oznaczona adnotacją @NonRestartableComposable lub @NonSkippableComposable.
  • wymagany parametr jest typu niestabilnego.

Istnieje tryb kompilatora, Strong Skipping, który łagodzi ostatni wymóg.

Aby typ był uznawany za stabilny, musi spełniać te warunki:

  • Wynik equals dla 2 instancji będzie zawsze taki sam w przypadku tych samych 2 instancji.
  • Jeśli zmieni się publiczna właściwość typu, kompozycja zostanie o tym powiadomiona.
  • Wszystkie typy właściwości publicznych są również stabilne.

Istnieją ważne typy, które spełniają te warunki i które kompilator Compose będzie traktować jako stabilne, mimo że nie są one wyraźnie oznaczone jako stabilne za pomocą adnotacji @Stable:

  • Wszystkie typy wartości pierwotnych: Boolean, Int, Long, Float, Char itp.
  • Ciągi znaków
  • Wszystkie typy funkcji (lambdy)

Wszystkie te typy mogą spełniać warunki stabilności, ponieważ są niezmienne. Ponieważ typy niezmienne nigdy się nie zmieniają, nie muszą powiadamiać kompozycji o zmianie, więc łatwiej jest spełnić te warunki.

Jednym z typów, który jest stabilny, ale jest zmienny, jest typ MutableState w Compose. Jeśli wartość jest przechowywana w MutableState, obiekt stanu jest ogólnie uważany za stabilny, ponieważ Compose będzie powiadamiany o wszelkich zmianach właściwości .value w State.

Gdy wszystkie typy przekazywane jako parametry do funkcji typu „composable” są stabilne, wartości parametrów są porównywane pod kątem równości na podstawie pozycji funkcji typu „composable” w drzewie interfejsu. Rekompozycja jest pomijana, jeśli wszystkie wartości są niezmienione od czasu poprzedniego wywołania.

Compose uważa typ za stabilny tylko wtedy, gdy może to udowodnić. Na przykład interfejs jest zwykle traktowany jako niestabilny, a typy ze zmiennymi właściwościami publicznymi, których implementacja może być niezmienna, również nie są stabilne.

Jeśli Compose nie może stwierdzić, że typ jest stabilny, ale chcesz wymusić na Compose traktowanie go jako stabilnego, oznacz go adnotacją @Stable.

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

W powyższym fragmencie kodu, ponieważ UiState jest interfejsem, Compose może zwykle uznać ten typ za niestabilny. Dodając adnotację @Stable, informujesz Compose, że ten typ jest stabilny, co pozwala Compose na preferowanie inteligentnego ponownego tworzenia. Oznacza to również, że jeśli interfejs jest używany jako typ parametru, Compose będzie traktować wszystkie jego implementacje jako stabilne.