Cykl życia elementów kompozycyjnych

Na tej stronie poznasz cykl życia elementu kompozycyjnego i sposób, w jaki decyduje on, czy taki element wymaga zmiany kompozycji.

Omówienie cyklu życia

Jak wspomnieliśmy w dokumentacji zarządzania stanem, kompozycja opisuje interfejs użytkownika aplikacji i jest tworzona przez uruchomienie funkcji kompozycyjnych. Kompozycja to struktura drzewa elementów kompozycyjnych, która opisuje Twój interfejs użytkownika.

Gdy Jetpack Compose po raz pierwszy uruchomi kompozycje po raz pierwszy, podczas początkowej kompozycji będzie śledzić te kompozycje, które wywołujesz, aby opisać Twój interfejs w ramach kompozycji. Następnie, gdy stan aplikacji się zmieni, Jetpack Compose planuje ponowną kompozycję. Zmiana ma miejsce, gdy Jetpack Compose ponownie wykonuje kompozycje, które mogły się zmienić w zależności od zmian stanu, a następnie aktualizuje kompozycję, aby uwzględnić ewentualne zmiany.

Kompozycję można utworzyć tylko na podstawie początkowej kompozycji i zaktualizować ją przez zmianę kompozycji. Jedynym sposobem zmodyfikowania kompozycji jest jej zmiana.

Diagram przedstawiający cykl życia funkcji kompozycyjnej

Rysunek 1. Cykl życia elementu kompozycyjnego w kompozycji. Uruchamia się, zostaje ponownie skomponowana co najmniej 0 razy i opuszcza kompozycję.

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

Jeśli funkcja kompozycyjna zostanie wywołana wiele razy, w kompozycji zostanie umieszczonych wiele jej instancji. Każde połączenie ma swój własny cykl życia w kompozycji.

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

Diagram przedstawiający hierarchiczne rozmieszczenie elementów w poprzednim fragmencie kodu

Rysunek 2. Element MyComposable w kompozycji. Jeśli funkcja kompozycyjna zostanie wywołana wiele razy, w kompozycji zostanie umieszczonych wiele jej instancji. Jeśli element ma inny kolor, jest to osobny element.

Anatomia elementu kompozycyjnego w sekcji Kompozycja

Wystąpienie funkcji kompozycyjnej w ramach kompozycji jest identyfikowane przez stronę wywołania. Kompilator Compose traktuje każdą witrynę generującą połączenia jako osobne. Wywołanie funkcji kompozycyjnej z wielu witryn wywołujących powoduje utworzenie wielu wystąpień elementu kompozycyjnego w elemencie Composition.

Jeśli podczas zmiany kompozycji funkcja kompozycyjna wywołuje inne kompozycje niż jej poprzednią kompozycję, funkcja zidentyfikuje takie, które zostały lub nie wywołała. Natomiast w przypadku tych, które były wywoływane w obu kompozycjach, funkcja unika ich ponownego tworzenia, jeśli ich dane wejściowe się nie zmieniły.

Zachowanie tożsamości ma kluczowe znaczenie dla powiązania efektów ubocznych z komponentem, aby można było je zakończyć pomyślnie, a nie uruchamiać ponownie dla każdej zmiany kompozycji.

Przeanalizuj ten przykład:

@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() { /* ... */ }

We fragmencie kodu powyżej LoginScreen warunkowo wywołuje funkcję LoginError kompozycyjną i zawsze będzie wywoływać element LoginInput kompozycyjny. Każde wywołanie ma unikalną pozycję witryny wywołania i pozycję źródła, której kompilator użyje do jej identyfikacji.

Diagram pokazujący, jak poprzedni kod jest rekomponowany, jeśli flaga showError zostanie zmieniona na true. Element kompozycyjny LoginError został dodany, ale pozostałe elementy kompozycyjne nie są tworzone ponownie.

Rysunek 3. Reprezentacja funkcji LoginScreen w kompozycji w przypadku zmiany stanu i zmiany kompozycji. Ten sam kolor oznacza, że element nie został ponownie skomponowany.

Mimo że instancja LoginInput została wywołana jako pierwsza, zostanie wywołana jako druga, instancja LoginInput zostanie zachowana we wszystkich rekompozycjach. Poza tym, ponieważ LoginInput nie ma żadnych parametrów, które zmieniły się w czasie zmiany kompozycji, wywołanie LoginInput zostanie pominięte przez funkcję Compose.

Dodaj więcej informacji, aby ułatwić inteligentne zmienianie kompozycji

Wielokrotne wywołanie funkcji kompozycyjnej spowoduje wielokrotne jej dodanie do kompozycji. W przypadku wielokrotnego wywoływania funkcji kompozycyjnej z tej samej witryny wywołania nie ma żadnych informacji umożliwiających jednoznaczną identyfikację każdego wywołania tego elementu kompozycyjnego, więc oprócz witryny wywołania jest używana kolejność wykonywania, tak aby instancje były różne. Takie zachowanie czasami nie wystarczy, ale w niektórych przypadkach może powodować niepożądane zachowanie.

@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 funkcja Compose korzysta z kolejności wykonania oprócz witryny wywołania, aby zapewnić odrębność instancji w ramach kompozycji. Jeśli nowy element movie zostanie dodany na dolnej części listy, funkcja Compose będzie mogła ponownie użyć wystąpień już znajdujących się w kompozycji, ponieważ ich lokalizacja na liście nie uległa zmianie i dlatego dane wejściowe movie są w tych instancjach takie same.

Diagram pokazujący, jak poprzedni kod jest rekomponowany, gdy na dole listy zostanie dodany nowy element. Inne elementy na liście nie zmieniły się i nie zostały utworzone ponownie.

Rysunek 4. Symbol MoviesScreen w kompozycji, gdy na dole listy zostanie dodany nowy element. MovieOverview kompozycji w Kompozycji można wykorzystać ponownie. Ten sam kolor w MovieOverview oznacza, że funkcja kompozycyjna nie została ponownie skomponowana.

Jeśli jednak lista movies zmieni się przez dodanie jej na górę lub środkową pozycję, usunięcie lub zmianę kolejności elementów, spowoduje to zmianę kompozycji wszystkich wywołań MovieOverview, których parametr wejściowy zmienił pozycję na liście. Jest to niezwykle ważne, jeśli na przykład MovieOverview pobiera obraz z filmu za pomocą efektu ubocznego. Jeśli zmiana kompozycji nastąpi w czasie, gdy efekt jest w toku, zostanie anulowany i rozpocznie się od nowa.

@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 rekomponowany, gdy na początku listy zostanie dodany nowy element. Wszystkie pozostałe elementy na liście zmieniają pozycję i trzeba je utworzyć ponownie.

Rysunek 5. Prezentacja właściwości MoviesScreen w kompozycji, gdy do listy dodano nowy element. MovieOverview komponentów kompozycyjnych nie można używać ponownie, a wszystkie efekty uboczne zostaną uruchomione ponownie. Inny kolor w MovieOverview oznacza, że funkcja kompozycyjna została skomponowana ponownie.

Idealnie chcemy traktować tożsamość instancji MovieOverview jako połączoną z tożsamością elementu movie, która jest do niej przekazywana. Gdy zmieniamy kolejność filmów na liście, najlepiej byłoby to zrobić w podobny sposób w instancji w drzewie kompozycji, zamiast ponownie komponować każdy element MovieOverview z inną instancją filmu. Tworzenie pozwala określić w środowisku wykonawczym, jakich wartości chcesz użyć do identyfikacji danej części drzewa: elementu kompozycyjnego key.

Dzięki pakowaniu bloku kodu wywołaniem klucza kompozycyjnego z co najmniej 1 przekazaną wartością wartości te zostaną połączone, aby można było zidentyfikować tę instancję w kompozycji. Wartość właściwości key nie musi być unikalna globalnie, a jedynie musi być unikalna wśród wywołań funkcji kompozycyjnych w witrynie wywołania. W tym przykładzie każdy obiekt movie musi mieć element key, który jest unikalny wśród elementów movies. Może też współdzielić ten element key z jakimś innym elementem kompozycyjnym w innym miejscu 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 jeśli elementy na liście ulegną zmianie, funkcja tworzenia rozpoznaje pojedyncze wywołania funkcji MovieOverview i może ich używać ponownie.

Diagram pokazujący, jak poprzedni kod jest rekomponowany, gdy na początku listy zostanie dodany nowy element. Elementy listy są identyfikowane za pomocą kluczy, więc funkcja tworzenia wie, aby nie tworzyć ich ponownie, mimo że zmieniły się ich pozycje.

Rysunek 6. Prezentacja właściwości MoviesScreen w kompozycji, gdy do listy dodano nowy element. Elementy kompozycyjne MovieOverview mają unikalne klucze, dlatego funkcja 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 kompozycyjne mają wbudowaną obsługę funkcji kompozycyjnej key. Na przykład LazyColumn umożliwia określenie niestandardowego parametru 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 zmiany kompozycji niektóre kwalifikujące się funkcje kompozycyjne mogą zostać całkowicie pominięte, jeśli ich dane wejściowe nie zmieniły się od poprzedniej kompozycji.

Funkcja kompozycyjna może zostać pominięta, chyba że:

  • Funkcja zwraca typ inny niż Unit
  • Funkcja jest opatrzona adnotacjami @NonRestartableComposable lub @NonSkippableComposable
  • Wymagany parametr ma niestabilny typ

Istnieje eksperymentalny tryb kompilatora Strong Pomiń, który łagodzi ostatnie wymaganie.

Aby dany typ został uznany za stabilny, musi być zgodny z tą umową:

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

Ta umowa obejmuje kilka ważnych, typowych typów, które kompilator wiadomości będzie traktować jako stabilne, mimo że nie są wyraźnie oznaczone jako stabilne za pomocą adnotacji @Stable:

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

Wszystkie te typy mogą być zgodne z umową stabilną, ponieważ są stałe. Typy stałe nigdy się nie zmieniają, więc nigdy nie muszą powiadamiać o zmianie.

Jednym z ważnych typów, który jest stabilny, ale zmienny, jest typ MutableState w komponencie. Jeśli wartość jest przechowywana w elemencie MutableState, ogólny obiekt stanu jest uznawany za stabilny, ponieważ funkcja Utwórz jest powiadamiana o wszelkich zmianach we właściwości .value obiektu State.

Gdy wszystkie typy przekazywane jako parametry do funkcji kompozycyjnej są stabilne, wartości parametrów są porównywane pod kątem równości na podstawie pozycji elementu kompozycyjnego w drzewie interfejsu. Zmiana kompozycji jest pomijana, jeśli wszystkie wartości pozostały niezmienione od poprzedniego wywołania.

Funkcja tworzenia jest uznawana za stabilną tylko wtedy, gdy jest w stanie to udowodnić. Na przykład interfejs jest zwykle traktowany jako niestabilny, a typy ze zmiennymi właściwościami publicznymi, których implementacja mogłaby być stała, również nie są stabilne.

Jeśli w przypadku tworzenia wiadomości nie uda się ustalić, że dany typ jest stabilny, ale chcesz zmusić go do traktowania go jako stabilnego, oznacz go za pomocą adnotacji @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
}

Ponieważ we fragmencie kodu powyżej UiState jest interfejsem, funkcja tworzenia może zwykle uznać ten typ za niestabilny. Dodając adnotację @Stable, informujesz Kompozycję, że ten typ jest stabilny, dzięki czemu faworyzujesz inteligentne zmiany kompozycji. Oznacza to również, że jeśli jako typu parametru będzie używany interfejs, funkcja Compose będzie traktować wszystkie jej implementacje jako stabilne.