Cykl życia elementów kompozycyjnych

Z tej strony dowiesz się więcej o cyklu życia komponentu oraz o tym, jak Compose decyduje, czy komponent wymaga ponownego skompilowania.

Omówienie cyklu życia

Jak wspomniano w dokumentacji dotyczącej zarządzania stanem, kompozycja opisuje interfejs aplikacji i jest generowana przez uruchamianie komponentów. Kompozycja to struktura drzewikowa komponentów, które opisują interfejs użytkownika.

Gdy w Jetpack Compose po raz pierwszy uruchomisz swoje kompozycje, w trakcie początkowej kompozycji zacznie śledzić te elementy, które wywołujesz, aby opisać interfejs w ramach kompozycji. Gdy stan aplikacji się zmieni, Jetpack Compose zaplanowa przetworzenie. Rekompozycja to sytuacja, w której Jetpack Compose ponownie wykonuje komponenty, które mogły ulec zmianie w odpowiedzi na zmiany stanu, a potem aktualizuje kompozycję, aby odzwierciedlić wszelkie zmiany.

Kompozycja może być wygenerowana tylko przez początkową kompozycję i zaktualizowana przez ponowne złożenie. Jedynym sposobem na zmodyfikowanie kompozycji jest jej ponowne kompozycje.

Diagram przedstawiający cykl życia komponentu

Rysunek 1. Cykl życia komponentu w kompozycji. Wchodzi do kompozycji, jest ponownie złożony co najmniej 0 razy i wychodzi z kompozycji.

Zmiana kompozycji jest zwykle wyzwalana przez zmianę obiektu State<T>. Funkcja Compose śledzi je i uruchamia w niej wszystkie elementy kompozycyjne, które odczytują określony element State<T>, oraz wszystkie wywoływane przez nie elementy kompozycyjne, których nie można pominąć.

Jeśli funkcja kompozycyjna zostanie wywołana wiele razy, w kompozycji znajdzie się ich więcej. Każde wywołanie ma własny cykl życia w kompozycji.

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

Diagram pokazujący hierarchiczny układ elementów w poprzednim fragmencie kodu

Rysunek 2. Przykład MyComposable w kompozycji. Jeśli kompozyt jest wywoływany wielokrotnie, w kompozycji umieszczane są jego liczne wystąpienia. Element o innym kolorze wskazuje, że jest to osobna instancja.

Struktura komponentu w komponentach

Wystąpienie kompozytowalnej funkcji w kompozycji jest identyfikowane przez jej miejsce wywołania. Kompilator Compose traktuje każde miejsce wywołania jako osobne. Wywoływanie komponentów z wielu miejsc wywołania spowoduje utworzenie wielu instancji komponentu w kompozycji.

Jeśli podczas ponownej kompozycji funkcja kompozycyjna wywołuje inne kompozycje niż w przypadku poprzedniej, funkcja Compose wskazuje, które kompozycje zostały wywołane, a które nie, a w przypadku elementów kompozycyjnych wywołanych w obu kompozycjach funkcja Compose unika ich ponownego utworzenia, jeśli dane wejściowe się nie zmienią.

Zachowanie tożsamości ma kluczowe znaczenie w kontekście powiązania efektów ubocznych z elementami kompozycyjnymi, tak aby były one prawidłowo realizowane, a nie wznawiane po każdej zmianie.

Przyjrzyjmy 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 wywołuje kompozyt LoginError warunkowo, a kompozyt LoginInput zawsze. Każde wywołanie ma unikalne miejsce wywołania i źródło, które kompilator wykorzysta do jego jednoznacznej identyfikacji.

Diagram pokazujący, jak poprzedni kod jest ponownie złożony, jeśli flaga showError zostanie zmieniona na wartość true. Dodano komponent LoginError, ale inne komponenty nie zostały ponownie złożone.

Rysunek 3. Reprezentacja LoginScreen w kompozycji, gdy stan się zmienia i występuje ponowne tworzenie kompozycji. Ten sam kolor oznacza, że nie został ponownie skomponowany.

Mimo że funkcja LoginInput została wywołana jako pierwsza, a potem jako druga, instancja LoginInput będzie zachowana w przypadku każdej rekompozycji. Dodatkowo funkcja LoginInput nie ma żadnych parametrów, które zmieniłyby się podczas rekompozycji, więc wywołanie funkcji LoginInput zostanie pominięte przez funkcję Compose.

Dodaj więcej informacji, które wspomogą inteligentne zmiany kompozycji

Wywoływanie kompozytowalnej wielokrotnie spowoduje również wielokrotne dodanie jej do kompozycji. Gdy wywołujesz kompozyt wielokrotnie z tego samego miejsca wywołania, Compose nie ma żadnych informacji umożliwiających jednoznaczne zidentyfikowanie każdego wywołania tego kompozytu, więc oprócz miejsca wywołania używana jest kolejność wykonania, aby zachować odrębność instancji. Czasami to wystarczy, ale w niektórych przypadkach może to 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 przykładzie powyżej kompozycja używa kolejności wykonywania oprócz miejsca wywołania, aby zachować odrębność instancji w kompozycji. Jeśli nowy element movie zostanie dodany do dołu listy, kompozytor może ponownie użyć instancji, które są już w kompozycji, ponieważ ich lokalizacja na liście się nie zmieniła, a więc dane wejściowe movie są takie same w przypadku tych instancji.

Schemat przedstawiający ponowne komponowanie poprzedniego kodu w przypadku dodania nowego elementu u dołu listy. Pozostałe elementy na liście nie zmieniły pozycji i nie zostały zmienione.

Rysunek 4. Przykład elementu MoviesScreen w kompozycji, gdy nowy element jest dodawany na końcu listy. Elementy kompozycyjne MovieOverview w kompozycji można ponownie wykorzystać. Ten sam kolor w komponowalnym elemencie MovieOverview oznacza, że nie został on ponownie skompilowany.

Jeśli jednak lista movies ulegnie zmianie, np. przez dodanie elementów do góry lub pośredniej pozycji na liście, usunięcie elementów lub zmianę ich kolejności, spowoduje to ponowne ułożenie wszystkich wywołań MovieOverview, których parametr wejściowy zmienił pozycję na liście. Jest to bardzo ważne, jeśli na przykład MovieOverview pobiera obraz filmu za pomocą efektu ubocznego. Jeśli ponowne skompilowanie nastąpi, gdy efekt jest w trakcie tworzenia, zostanie anulowany i rozpocznie 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 składany, gdy nowy element zostanie dodany na szczyt listy. Każdy inny element na liście zmienia pozycję i trzeba ją utworzyć ponownie.

Rysunek 5. Przykładowy element MoviesScreen w kompozycji, gdy do listy dodano nowy element. Elementów kompozycyjnych MovieOverview nie można użyć ponownie, a wszystkie efekty uboczne zostaną uruchomione ponownie. Inny kolor w MovieOverview oznacza, że usługa została ponownie skompilowana.

Najlepiej jest traktować tożsamość wystąpienia MovieOverview jako powiązaną z tożsamością przekazanego mu obiektu movie. Gdybyśmy zmienili kolejność filmów na liście, najlepiej byłoby w podobny sposób zmienić kolejność wystąpień w drzewie kompozycji zamiast ponownie komponować każdy element MovieOverview z innym wystąpieniem filmu. Dzięki funkcji Compose możesz określić w czasie wykonywania, których wartości chcesz używać do identyfikowania danej części drzewa: kompozyt key.

Owijanie bloku kodu w wywołanie kluczowego komponentu z jedną lub większą liczbą przekazanych wartości powoduje, że te wartości zostaną połączone i użyte do zidentyfikowania tego wystąpienia w kompozycji. Wartość key nie musi być globalnie unikalna. Musi być niepowtarzalna tylko wśród wywołań elementów kompozycyjnych w witrynie wywołania. W tym przykładzie każda movie musi mieć key, który jest unikalny wśród movies. Może się ona dzielić tym key z inną kompozycją w 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 wtedy, gdy elementy na liście się zmienią, Compose rozpoznaje poszczególne wywołania funkcji MovieOverview i może ich używać ponownie.

Diagram pokazujący, jak poprzedni kod jest ponownie składany, gdy nowy element zostanie dodany na szczyt listy. Elementy listy są identyfikowane za pomocą kluczy, więc Compose wie, że nie musi ich ponownie komponować, nawet jeśli ich pozycje się zmieniły.

Rysunek 6. Przykładowy element MoviesScreen w kompozycji, gdy do listy dodano nowy element. Składniki MovieOverview mają unikalne klucze, więc Compose rozpoznaje, które instancje MovieOverview się nie zmieniły, i może ich używać ponownie. Ich efekty uboczne będą nadal wykonywane.

Niektóre komponenty mają wbudowaną obsługę komponentu key. Na przykład LazyColumn akceptuje określenie niestandardowego elementu 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 tworzenia kompozycji można całkowicie pominąć wykonanie niektórych funkcji kompozytowych, jeśli ich dane wejściowe nie zmieniły się od poprzedniej kompozycji.

Funkcja typu „composable” może być pomijana chyba że:

  • Funkcja ma typ zwracany inny niż Unit.
  • Funkcja jest opatrzona adnotacją @NonRestartableComposable lub @NonSkippableComposable
  • Wymagany parametr jest niestabilny

Dostępny jest eksperymentalny tryb kompilatora, Strong Skipping, który łagodzi ostatnie wymaganie.

Aby typ został uznany za stabilny, musi być zgodny z tym dokumentem:

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

Istnieje kilka ważnych typowych typów objętych tą umową, które kompilator będzie traktować jako stabilny, mimo że nie są one wyraźnie oznaczone jako stabilne za pomocą adnotacji @Stable:

  • Wszystkie typy wartości prymitywnych: Boolean, Int, Long, Float, Char itp.
  • Strings
  • Wszystkie typy funkcji (lambda)

Wszystkie te typy mogą przestrzegać umowy stabilnej, ponieważ są niezmienne. Typy niezmienne nigdy się nie zmieniają, więc nie trzeba powiadamiać Composition o zmianach, co znacznie ułatwia przestrzeganie tego kontraktu.

Warto zwrócić uwagę na typ MutableState, który jest stabilny, ale może ulec zmianie. Jeśli wartość jest przechowywana w MutableState, obiekt stanu ogólnego jest uważany za stabilny, ponieważ funkcja tworzenia wiadomości będzie otrzymywać powiadomienia o wszelkich zmianach we właściwości .value obiektu State.

Gdy wszystkie typy przekazane jako parametry do komponentu są stabilne, wartości parametrów są porównywane pod kątem równości na podstawie pozycji komponentu w drzewie interfejsu użytkownika. Rekompozycja jest pomijana, jeśli wszystkie wartości są takie same jak w poprzednim wywołaniu.

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

Jeśli Compose nie jest w stanie stwierdzić, że typ jest stabilny, ale chcesz zmusić Compose do traktowania 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
}

We fragmencie kodu powyżej UiState jest interfejsem, więc usługa Compose zwykle może uznać ten typ za niestabilny. Dodając adnotację @Stable, informujesz funkcję tworzenia wiadomości, że ten typ jest stabilny, dzięki czemu funkcja tworzenia wiadomości będzie faworyzować inteligentne zmiany kompozycji. Oznacza to też, że Compose będzie traktować wszystkie implementacje jako stabilne, jeśli interfejs jest używany jako typ parametru.