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.
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") } }
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.
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.
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) /* ... */ } }
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ć.
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.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Stan i Jetpack Compose
- Efekty uboczne w Compose
- Zapisywanie stanu interfejsu w Compose