Cykl życia elementów kompozycyjnych

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

Omówienie cyklu życia

Jak wspomnieliśmy w dokumentacji dotyczącej zarządzania stanem, kompozycja opisuje interfejs aplikacji i jest tworzona przez uruchamianie funkcji kompozycyjnych. Kompozycja to struktura drzewa elementów kompozycyjnych, które opisują interfejs.

Gdy Jetpack Compose po raz pierwszy uruchamia funkcje kompozycyjne podczas początkowej kompozycji, śledzi funkcje kompozycyjne, które wywołujesz, aby opisać interfejs w kompozycji. Gdy stan aplikacji się zmieni, Jetpack Compose zaplanuje ponowne komponowanie. Ponowne komponowanie to ponowne wykonywanie przez Jetpack Compose funkcji kompozycyjnych, które mogły ulec zmianie w odpowiedzi na zmiany stanu, a następnie aktualizowanie kompozycji w celu odzwierciedlenia tych zmian.

Kompozycję można utworzyć tylko na podstawie kompozycji początkowej i zaktualizować przez ponowne skomponowanie. Kompozycję można modyfikować tylko przez ponowne komponowanie.

Diagram przedstawiający cykl życia komponentu

Rysunek 1. Cykl życia funkcji kompozycyjnej w kompozycji. Wchodzi do kompozycji, jest ponownie komponowany 0 lub więcej razy i opuszcza kompozycję.

Ponowne komponowanie jest zwykle wywoływane przez zmianę obiektu State<T>. Funkcja Compose śledzi te elementy i uruchamia wszystkie funkcje kompozycyjne w kompozycji, które odczytują ten konkretny State<T>, oraz wszystkie funkcje kompozycyjne, które wywołują i których nie można pominąć.

Jeśli funkcja kompozycyjna jest wywoływana wiele razy, w kompozycji umieszczanych jest wiele jej instancji. Każde połączenie 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 kompozycyjna jest wywoływana wiele razy, w kompozycji umieszczanych jest wiele instancji. Element o innym kolorze wskazuje, że jest to osobna instancja.

Anatomia funkcji kompozycyjnej w kompozycji

Instancja funkcji kompozycyjnej w kompozycji jest identyfikowana przez jej miejsce wywołania. Kompilator Compose traktuje każde miejsce wywołania jako odrębne. Wywoływanie funkcji kompozycyjnych z wielu miejsc wywołania spowoduje utworzenie w kompozycji wielu instancji funkcji kompozycyjnej.

Jeśli podczas ponownego komponowania funkcja kompozycyjna wywołuje inne funkcje kompozycyjne niż podczas poprzedniego komponowania, Compose określi, które funkcje kompozycyjne zostały wywołane, a które nie. W przypadku funkcji kompozycyjnych wywołanych w obu przypadkach Compose uniknie ponownego komponowania, jeśli ich dane wejściowe nie uległy zmianie.

Zachowanie tożsamości jest kluczowe, aby powiązać efekty uboczne z funkcją kompozycyjną, dzięki czemu mogą one zostać ukończone, a nie uruchamiane ponownie przy każdej rekompozycji.

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ła funkcję kompozycyjną LoginError, a funkcję kompozycyjną LoginInput wywoła zawsze. Każde wywołanie ma unikalne miejsce wywołania i pozycję źródłową, których kompilator używa do jego jednoznacznej identyfikacji.

Diagram pokazujący, jak poprzedni kod zostanie przekształcony, jeśli flaga showError zostanie zmieniona na true. Kompozycja LoginError jest dodawana, ale pozostałe kompozycje nie są ponownie komponowane.

Rysunek 3. Reprezentacja LoginScreen w kompozycji, gdy stan ulega zmianie i następuje ponowna kompozycja. Ten sam kolor oznacza, że nie został on ponownie skomponowany.

Mimo że LoginInput zmienił kolejność wywoływania z pierwszej na drugą, instancja LoginInput zostanie zachowana w przypadku ponownego komponowania. Dodatkowo, ponieważ funkcja LoginInput nie ma żadnych parametrów, które uległy zmianie w wyniku ponownego komponowania, wywołanie funkcji LoginInput zostanie pominięte przez Compose.

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

Wielokrotne wywołanie funkcji kompozycyjnej spowoduje jej wielokrotne dodanie do kompozycji. Gdy funkcja kompozycyjna jest wywoływana wielokrotnie 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 odróżnić poszczególne instancje. Czasami takie działanie jest wystarczające, ale w niektórych przypadkach może powodować niepożądane zachowania.

@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 wykonania oprócz miejsca wywołania, aby zachować odrębność instancji w kompozycji. Jeśli nowy element movie zostanie dodany na dole listy, funkcja tworzenia może ponownie użyć instancji, które już znajdują się w kompozycji, ponieważ ich położenie na liście nie uległo zmianie, a dane wejściowe movie są w przypadku tych instancji takie same.

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 elementu MoviesScreen w kompozycji po dodaniu nowego elementu na końcu listy. Funkcje kompozycyjne MovieOverview w kompozycji można wykorzystywać ponownie. Ten sam kolor w MovieOverview oznacza, że komponent nie został ponownie skomponowany.

Jeśli jednak lista movies zmieni się w wyniku dodania elementów na początku lub w środku listy, usunięcia lub zmiany kolejności elementów, spowoduje to ponowne wygenerowanie wszystkich wywołań MovieOverview, których parametr wejściowy zmienił pozycję na liście. Jest to niezwykle ważne, jeśli na przykład funkcja MovieOverview pobiera obraz filmu za pomocą efektu ubocznego. Jeśli ponowne komponowanie nastąpi w trakcie działania efektu, zostanie ono anulowane 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 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 elementu MoviesScreen w kompozycji po dodaniu nowego elementu do listy. Funkcji kompozycyjnych MovieOverview nie można ponownie wykorzystać, a wszystkie efekty uboczne zostaną uruchomione ponownie. Inny kolor w MovieOverview oznacza, że komponent został ponownie skomponowany.

Najlepiej byłoby, gdyby tożsamość MovieOverview wystąpienia była powiązana z tożsamością movie przekazywaną do niego. Jeśli zmienimy kolejność filmów na liście, najlepiej będzie zmienić kolejność instancji w drzewie kompozycji, zamiast ponownie komponować każdy element kompozycyjny MovieOverview z inną instancją filmu. Compose umożliwia przekazywanie środowisku wykonawczemu informacji o wartościach, których chcesz używać do identyfikowania danej części drzewa: funkcji kompozycyjnej key.

Jeśli blok kodu zostanie otoczony wywołaniem funkcji klucza z przekazanymi co najmniej 1 wartością, te wartości zostaną połączone w celu identyfikacji tej instancji w kompozycji. Wartość key nie musi być unikalna globalnie, wystarczy, że będzie unikalna wśród wywołań funkcji kompozycyjnych 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 innym komponentem 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 się zmienią, Compose rozpozna poszczególne wywołania funkcji MovieOverview i będzie mógł 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 elementu MoviesScreen w kompozycji po dodaniu nowego elementu do listy. Ponieważ funkcje kompozycyjne MovieOverview mają unikalne klucze, Compose rozpoznaje, które instancje MovieOverview nie uległy zmianie, i może ich użyć ponownie. Ich efekty uboczne będą nadal wykonywane.

Niektóre funkcje kompozycyjne mają wbudowaną obsługę funkcji kompozycyjnej key. Na przykład funkcja LazyColumn akceptuje określanie niestandardowego parametru key w języku items DSL.

@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 komponowania niektóre kwalifikujące się funkcje kompozycyjne mogą zostać całkowicie pominięte, jeśli ich dane wejściowe nie zmieniły się od poprzedniego komponowania.

Funkcja typu „composable” może zostać pominięta chyba że:

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

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

Aby typ został uznany za stabilny, musi spełniać te wymagania:

  • Wynik funkcji equals dla 2 instancji będzie zawsze taki sam w przypadku tych samych 2 instancji.
  • Jeśli zmieni się publiczna właściwość danego typu, komponent Composition otrzyma powiadomienie.
  • Wszystkie typy właściwości publicznych są też stabilne.

Istnieją ważne typy wspólne, które wchodzą w zakres tego kontraktu i są traktowane przez kompilator Compose jako stabilne, mimo że nie są wyraźnie oznaczone jako stabilne za pomocą adnotacji @Stable:

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

Wszystkie te typy mogą być zgodne z umową dotyczącą stabilności, ponieważ są niezmienne. Typy niezmienne nigdy się nie zmieniają, więc nie muszą powiadamiać o zmianie funkcji Composition, dzięki czemu łatwiej jest przestrzegać tego kontraktu.

Jednym z godnych uwagi typów, który jest stabilny, ale można go zmieniać, jest typ MutableState w Compose. Jeśli wartość jest przechowywana w MutableState, obiekt stanu jest ogólnie uznawany za stabilny, ponieważ Compose będzie powiadamiany o wszelkich zmianach właściwości .value obiektu State.

Gdy wszystkie typy przekazane jako parametry do funkcji kompozycyjnej są stabilne, wartości parametrów są porównywane pod kątem równości na podstawie pozycji funkcji kompozycyjnej w drzewie interfejsu. Ponowne komponowanie jest pomijane, jeśli wszystkie wartości nie uległy zmianie od poprzedniego wywołania.

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

Jeśli Compose nie może wywnioskować, że typ jest stabilny, ale chcesz wymusić, aby Compose traktował go jako stabilny, 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 preferować inteligentne ponowne kompozycje. Oznacza to również, że jeśli interfejs jest używany jako typ parametru, Compose będzie traktować wszystkie jego implementacje jako stabilne.