Na tej stronie 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 Jetpack Compose uruchamia komponenty po raz pierwszy, podczas pierwotnej kompozycji śledzi komponenty wywoływane do opisu interfejsu użytkownika w 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 utworzenie.
Rysunek 1. Cykl życia komponenta w kompozycji. Wchodzi do kompozycji, jest w niej ponownie złożony co najmniej 0 razy i z niej wychodzi.
Rekompozycja jest zwykle wywoływana przez zmianę obiektu State<T>
. Compose śledzi te wartości i uruchamia wszystkie komponenty w kompozycji, które odczytują dany State<T>
, oraz wszystkie komponenty, które są przez nie wywoływane i których nie można pominieć.
Jeśli kompozyt został wywołany kilka razy, w kompozycji zostanie umieszczonych kilka jego wystąpień. Każde wywołanie ma własny cykl życia w kompozycji.
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
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.
Anatomia kompozytowa w komponowanych
Wystąpienie kompozytowalnej funkcji w kompozycji jest identyfikowane przez jej miejsce wywołania. Kompilator Compose traktuje każde miejsce wywołania jako odrębne. Wywoływanie komponentów z wielu miejsc wywołania spowoduje utworzenie wielu instancji komponentu w kompozycji.
Jeśli podczas ponownego tworzenia kompozytowa wywołuje inne kompozytowe niż podczas poprzedniego tworzenia, Compose określa, które kompozytowe zostały wywołane lub nie, a w przypadku kompozytowych wywołanych w obu kompozycjach Compose nie będzie ich ponownie tworzyć, jeśli ich dane wejściowe się nie zmieniły.
Zachowanie tożsamości jest kluczowe, aby efekty uboczne były powiązane z kompozycją, dzięki czemu mogą zostać ukończone, zamiast uruchamiać się ponownie przy każdej rekompozycji.
Rozważ 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() { /* ... */ }
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.
Rysunek 3. Reprezentacja LoginScreen
w kompozycji, gdy stan się zmienia i występuje ponowne tworzenie kompozycji. Ten sam kolor oznacza, że nie została ona ponownie złożona.
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.
Dodawanie dodatkowych informacji, które ułatwiają inteligentne zmiany składu
Wywoływanie kompozytowanego komponenta wiele razy spowoduje jego wielokrotne dodanie 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 wystarczy takie zachowanie, ale w niektórych przypadkach może ono 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ć wystąpień już obecnych 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 wystąpień.
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) /* ... */ } }
Rysunek 5. Przykładowy element MoviesScreen
w kompozycji, gdy do listy dodano nowy element. Komponentów MovieOverview
nie można ponownie wykorzystać, a wszystkie efekty uboczne zostaną ponownie uruchomione. Inny kolor w MovieOverview
oznacza, że usługa została ponownie skompilowana.
Najlepiej jest rozpatrywać tożsamość wystąpienia MovieOverview
jako powiązaną z tożsamością przekazanego mu obiektu movie
. Jeśli zmienimy kolejność listy filmów, w idealnym przypadku kolejność instancji w drzewie kompozycji powinna zostać odpowiednio zmieniona, zamiast ponownie tworzyć każdą kompozycję MovieOverview
z inną instancją 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ć tylko unikalna wśród wywołań kompozytowych w miejscu 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.
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 funkcja LazyColumn
akceptuje niestandardowe ustawienie key
w języku opisu usługi 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 ma niestabilny typ
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 publiczna usługa tego typu ulegnie zmianie, Composition zostanie o tym powiadomione.
- Wszystkie publiczne typy usług są też stabilne.
Istnieją ważne typy wspólne, które mieszczą się w ramach tego kontraktu i będą traktowane przez kompilator Compose jako stabilne, mimo że nie są 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 jest ogólnie uważany za stabilny, ponieważ usługa Compose będzie powiadamiana o wszelkich zmianach 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, 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 UiState
jest interfejsem, więc Compose może uznać ten typ za niestabilny. Dodając adnotację @Stable
, informujesz Compose, że ten typ jest stabilny, co pozwala Compose stosować inteligentne przekształcenia. Oznacza to też, że Compose będzie traktować wszystkie implementacje jako stabilne, jeśli interfejs jest używany jako typ parametru.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Stan i Jetpack Compose
- Efekty uboczne w edytorze
- Zapisywanie stanu interfejsu w sekcji Utwórz