Podobnie jak większość innych zestawów narzędzi UI, Compose renderuje klatkę w kilku różnych fazach. Na przykład system widoków Androida ma 3 główne fazy: pomiar, układ i rysowanie. Compose jest bardzo podobny, ale na początku ma ważną dodatkową fazę o nazwie kompozycja.
Więcej informacji o kompozycji znajdziesz w dokumentacji Compose w artykułach Myślenie w Compose oraz Stan i Jetpack Compose.
3 fazy klatki
Compose ma 3 główne fazy:
- Kompozycja: co ma być wyświetlane. Compose uruchamia funkcje typu „composable” i tworzy opis interfejsu.
- Układ: gdzie umieścić interfejs. Ta faza składa się z 2 etapów: pomiaru i umieszczenia. Elementy układu mierzą i umieszczają siebie oraz wszystkie elementy podrzędne we współrzędnych 2D dla każdego węzła w drzewie układu.
- Rysowanie: jak renderować. Elementy interfejsu rysują się na Canvas, zwykle na ekranie urządzenia.
Kolejność tych faz jest zwykle taka sama, co umożliwia przepływ danych w jednym
kierunku – od kompozycji przez układ do rysowania – w celu utworzenia klatki (znanej też
jako jednokierunkowy przepływ danych). BoxWithConstraints, LazyColumn,
i LazyRow to godne uwagi wyjątki, w których kompozycja elementów podrzędnych
zależy od fazy układu elementu nadrzędnego.
Teoretycznie każda z tych faz występuje w każdej klatce. Aby jednak zoptymalizować wydajność, Compose unika powtarzania pracy, która w tych wszystkich fazach dawałaby te same wyniki przy tych samych danych wejściowych. Compose pomija uruchamianie funkcji typu „composable”, jeśli może ponownie wykorzystać poprzedni wynik, a interfejs Compose nie zmienia układu ani nie rysuje ponownie całego drzewa, jeśli nie jest to konieczne. Compose wykonuje tylko minimalną ilość pracy wymaganą do zaktualizowania interfejsu. Ta optymalizacja jest możliwa, ponieważ Compose śledzi odczyty stanu w różnych fazach.
Omówienie faz
W tej sekcji bardziej szczegółowo opisujemy, jak są wykonywane 3 fazy Compose w przypadku funkcji typu „composable”.
Kompozycja
W fazie kompozycji środowisko wykonawcze Compose wykonuje funkcje typu „composable” i tworzy strukturę drzewa, która reprezentuje interfejs. To drzewo UI składa się z węzłów układu, które zawierają wszystkie informacje potrzebne do kolejnych faz, jak pokazano w tym filmie:
Rysunek 2. Drzewo reprezentujące interfejs, które jest tworzone w fazie kompozycji.
Podsekcja kodu i drzewa UI wygląda tak:
W tych przykładach każda funkcja typu „composable” w kodzie odpowiada jednemu węzłowi układu w drzewie UI. W bardziej złożonych przykładach funkcje typu „composable” mogą zawierać logikę i przepływ sterowania oraz tworzyć inne drzewo w zależności od stanu.
Układ
W fazie układu Compose używa jako danych wejściowych drzewa UI utworzonego w fazie kompozycji. Zbiór węzłów układu zawiera wszystkie informacje potrzebne do określenia rozmiaru i położenia każdego węzła w przestrzeni 2D.
Rysunek 4. Pomiar i umieszczenie każdego węzła układu w drzewie UI w fazie układu.
W fazie układu drzewo jest przeszukiwane za pomocą tego 3-etapowego algorytmu:
- Mierzenie elementów podrzędnych: węzeł mierzy swoje węzły podrzędne, jeśli takie istnieją.
- Określanie własnego rozmiaru: na podstawie tych pomiarów węzeł określa swój rozmiar.
- Umieszczanie elementów podrzędnych: każdy węzeł podrzędny jest umieszczany względem własnej pozycji węzła.
Na końcu tej fazy każdy węzeł układu ma:
- przypisaną szerokość i wysokość ;
- współrzędne x, y , w których ma być narysowany.
Przypomnij sobie drzewo UI z poprzedniej sekcji:
W przypadku tego drzewa algorytm działa tak:
Rowmierzy swoje elementy podrzędne, czyliImageiColumn.- Mierzony jest element
Image. Nie ma on elementów podrzędnych, więc określa swój rozmiar i zgłasza go do elementuRow. - Następnie mierzony jest element
Column. Najpierw mierzy on swoje elementy podrzędne (2 funkcje typu „composable”Text). - Mierzona jest pierwsza funkcja
Text. Nie ma ona elementów podrzędnych, więc określa swój rozmiar i zgłasza go do elementuColumn.- Mierzona jest druga funkcja
Text. Nie ma ona elementów podrzędnych, więc określa swój rozmiar i zgłasza go do elementuColumn.
- Mierzona jest druga funkcja
- Element
Columnużywa pomiarów elementów podrzędnych do określenia swojego rozmiaru. Używa maksymalnej szerokości elementu podrzędnego i sumy wysokości elementów podrzędnych. - Element
Columnumieszcza swoje elementy podrzędne względem siebie, umieszczając je pionowo jeden pod drugim. - Element
Rowużywa pomiarów elementów podrzędnych do określenia swojego rozmiaru. Używa maksymalnej wysokości elementu podrzędnego i sumy szerokości elementów podrzędnych. Następnie umieszcza swoje elementy podrzędne.
Pamiętaj, że każdy węzeł został odwiedzony tylko raz. Środowisko wykonawcze Compose wymaga tylko jednego przejścia przez drzewo UI, aby zmierzyć i umieścić wszystkie węzły, co zwiększa wydajność. Gdy liczba węzłów w drzewie rośnie, czas potrzebny na jego przeszukanie rośnie liniowo. Natomiast jeśli każdy węzeł byłby odwiedzany wielokrotnie, czas przeszukiwania rósłby wykładniczo.
Rysowanie
W fazie rysowania drzewo jest ponownie przeszukiwane od góry do dołu, a każdy węzeł rysuje się na ekranie.
Rysunek 5. W fazie rysowania rysowane są piksele na ekranie.
W poprzednim przykładzie zawartość drzewa jest rysowana w ten sposób:
- Element
Rowrysuje wszystkie treści, które może zawierać, np. kolor tła. - Element
Imagerysuje się. - Element
Columnrysuje się. - Pierwsza i druga funkcja
Textrysują się.
Rysunek 6. Drzewo UI i jego narysowana reprezentacja.
Odczyty stanu
Gdy w jednej z wymienionych wcześniej faz odczytujesz value elementu snapshot state, Compose automatycznie śledzi, co robił, gdy odczytywał element value. To śledzenie umożliwia Compose ponowne wykonanie czytnika, gdy zmieni się value stanu, i jest podstawą obserwacji stanu w Compose.
Stan zwykle tworzysz za pomocą funkcji mutableStateOf(), a następnie uzyskujesz do niego dostęp na 2 sposoby: bezpośrednio przez dostęp do właściwości value lub za pomocą delegata właściwości Kotlin. Więcej informacji na ten temat znajdziesz w artykule Stan w funkcjach kompozycyjnych. Na potrzeby tego przewodnika „odczyt stanu” odnosi się do jednej z tych równoważnych metod dostępu.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
W delegacie właściwości do uzyskiwania dostępu do value stanu i jego aktualizowania używane są funkcje „getter” i „setter”
. Te funkcje getter i setter są wywoływane tylko wtedy, gdy odwołujesz się do właściwości jako wartości, a nie wtedy, gdy jest ona tworzona. Dlatego 2 opisane wcześniej sposoby są równoważne.
Każdy blok kodu, który można ponownie wykonać, gdy zmieni się stan odczytu, jest zakresem ponownego uruchomienia. Compose śledzi zmiany value stanu i zakresy ponownego uruchomienia w różnych fazach.
Odczyty stanu w fazach
Jak wspomnieliśmy wcześniej, w Compose są 3 główne fazy, a Compose śledzi, jaki stan jest odczytywany w każdej z nich. Dzięki temu Compose może powiadamiać tylko te fazy, które muszą wykonać pracę w przypadku każdego elementu interfejsu.
W sekcjach poniżej opisujemy każdą fazę i to, co się dzieje, gdy w jej obrębie odczytywana jest wartość stanu.
Faza 1. Kompozycja
Odczyty stanu w funkcji @Composable lub bloku lambda wpływają na kompozycję i potencjalnie na kolejne fazy. Gdy zmieni się value stanu, funkcja recomposer zaplanuje ponowne uruchomienie wszystkich funkcji typu „composable”, które odczytują value tego stanu. Pamiętaj, że środowisko wykonawcze może pominąć niektóre lub wszystkie funkcje typu „composable”, jeśli dane wejściowe się nie zmieniły. Więcej informacji znajdziesz w sekcji Pomijanie, jeśli dane wejściowe
się nie zmieniły.
W zależności od wyniku kompozycji interfejs Compose uruchamia fazy układu i rysowania. Może pominąć te fazy, jeśli treść pozostanie taka sama, a rozmiar i układ się nie zmienią.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Faza 2. Układ
Faza układu składa się z 2 etapów: pomiaru i umieszczenia. Etap pomiaru uruchamia m.in. lambdę pomiaru przekazaną do funkcji typu „composable” Layout oraz metodę
MeasureScope.measure interfejsu LayoutModifier.
Etap umieszczenia uruchamia blok umieszczenia funkcji layout, blok lambda funkcji Modifier.offset { … } i podobne funkcje.
Odczyty stanu na każdym z tych etapów wpływają na układ i potencjalnie na fazę rysowania. Gdy zmieni się value stanu, interfejs Compose zaplanuje fazę układu. Jeśli zmieni się rozmiar lub pozycja, uruchomi też fazę rysowania.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Faza 3. Rysowanie
Odczyty stanu w kodzie rysowania wpływają na fazę rysowania. Typowe przykłady to Canvas(), Modifier.drawBehind i Modifier.drawWithContent. Gdy zmieni się value stanu, interfejs Compose uruchomi tylko fazę rysowania.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Optymalizowanie odczytów stanu
Ponieważ Compose wykonuje lokalne śledzenie odczytu stanu, możesz zminimalizować ilość pracy wykonywanej przez odczytywanie każdego stanu w odpowiedniej fazie.
Rozważ ten przykład. Ten przykład zawiera element Image(), który używa modyfikatora przesunięcia do przesunięcia jego ostatecznej pozycji układu, co powoduje efekt paralaksy podczas przewijania przez użytkownika.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Ten kod działa, ale powoduje suboptymalną wydajność. W takiej postaci kod
odczytuje value stanu firstVisibleItemScrollOffset i przekazuje go do
funkcji Modifier.offset(offset: Dp). Gdy użytkownik przewija, value elementu firstVisibleItemScrollOffset będzie się zmieniać. Jak już wiesz, Compose śledzi wszystkie odczyty stanu, aby móc ponownie uruchomić (ponownie wywołać) kod odczytu, który w tym przykładzie jest zawartością elementu Box.
To przykład odczytywania stanu w fazie kompozycji. Niekoniecznie jest to złe, a wręcz przeciwnie – jest to podstawa ponownej kompozycji, która umożliwia wyświetlanie nowego interfejsu po zmianie danych.
Ważne: ten przykład jest suboptymalny, ponieważ każde zdarzenie przewijania powoduje ponowną ocenę, pomiar, układ i wreszcie narysowanie całej zawartości funkcji typu „composable”. Faza Compose jest wywoływana przy każdym przewijaniu, mimo że wyświetlana treść się nie zmieniła, tylko jej pozycja. Możesz zoptymalizować odczyt stanu, aby ponownie wywoływać tylko fazę układu.
Przesunięcie z lambdą
Dostępna jest inna wersja modyfikatora przesunięcia:
Modifier.offset(offset: Density.() -> IntOffset).
Ta wersja przyjmuje parametr lambda, w którym wynikowe przesunięcie jest zwracane przez blok lambda. Zaktualizuj kod, aby go używać:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Dlaczego jest to bardziej wydajne? Blok lambda, który przekazujesz do modyfikatora, jest wywoływany w fazie układu (a konkretnie na etapie umieszczenia w fazie układu), co oznacza, że stan firstVisibleItemScrollOffset nie jest już odczytywany podczas kompozycji. Ponieważ Compose śledzi, kiedy stan jest odczytywany, ta zmiana oznacza, że jeśli zmieni się value elementu firstVisibleItemScrollOffset, Compose musi tylko ponownie uruchomić fazy układu i rysowania.
Oczywiście często jest absolutnie konieczne odczytywanie stanów w fazie kompozycji. Mimo to w niektórych przypadkach możesz zminimalizować liczbę ponownych kompozycji, filtrując zmiany stanu. Więcej informacji na ten temat znajdziesz w artykule
zobacz derivedStateOf: przekształcanie jednego lub kilku obiektów stanu w inny
stan.
Pętla ponownej kompozycji (cykliczna zależność faz)
W tym przewodniku wspomnieliśmy wcześniej, że fazy Compose są zawsze wywoływane w tej samej kolejności i że w tej samej klatce nie można się cofnąć. Nie uniemożliwia to jednak aplikacjom wchodzenia w pętle kompozycji w różnych klatkach. Rozważ ten przykład:
Box { var imageHeightPx by remember { mutableIntStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
Ten przykład implementuje kolumnę pionową z obrazem u góry i tekstem pod nim. Używa funkcji Modifier.onSizeChanged() do uzyskania rozwiązanego rozmiaru obrazu, a następnie używa funkcji Modifier.padding() na tekście, aby go przesunąć w dół.
Nienaturalna konwersja z Px z powrotem na Dp wskazuje już, że kod ma jakiś problem.
Problem z tym przykładem polega na tym, że kod nie osiąga „ostatecznego” układu w jednej klatce. Kod opiera się na wystąpieniu wielu klatek, co powoduje niepotrzebną pracę i sprawia, że interfejs przeskakuje na ekranie.
Kompozycja pierwszej klatki
W fazie kompozycji pierwszej klatki imageHeightPx ma początkowo wartość 0. W związku z tym kod udostępnia tekst z Modifier.padding(top = 0).
Kolejna faza układu wywołuje wywołanie zwrotne modyfikatora onSizeChanged, które aktualizuje imageHeightPx do rzeczywistej wysokości obrazu. Compose planuje następnie ponowną kompozycję na następną klatkę. Jednak w bieżącej fazie rysowania tekst jest renderowany z dopełnieniem 0, ponieważ zaktualizowana wartość imageHeightPx nie jest jeszcze uwzględniona.
Kompozycja drugiej klatki
Compose inicjuje drugą klatkę, wywołaną przez zmianę wartości imageHeightPx. W fazie kompozycji tej klatki stan jest odczytywany w bloku treści Box. Tekst ma teraz dopełnienie, które dokładnie odpowiada wysokości obrazu. W fazie układu imageHeightPx jest ponownie ustawiany, ale nie jest planowana żadna dalsza ponowna kompozycja, ponieważ wartość pozostaje spójna.
Ten przykład może się wydawać naciągany, ale uważaj na ten ogólny wzorzec:
Modifier.onSizeChanged(),onGloballyPositioned()lub inne operacje układu.- Aktualizowanie stanu.
- Używanie tego stanu jako danych wejściowych do modyfikatora układu (
padding(),height()lub podobnego). - Potencjalne powtórzenie.
Rozwiązaniem w przypadku poprzedniego przykładu jest użycie odpowiednich elementów pierwotnych układu. Poprzedni przykład można zaimplementować za pomocą funkcji Column(), ale możesz mieć bardziej złożony przykład, który wymaga czegoś niestandardowego, co będzie wymagało napisania niestandardowego układu. Więcej informacji znajdziesz w przewodniku Niestandardowe układy.
Ogólna zasada polega na tym, aby mieć jedno źródło wiarygodnych danych dla wielu elementów interfejsu, które powinny być mierzone i umieszczane względem siebie. Użycie odpowiedniego elementu pierwotnego układu lub utworzenie niestandardowego układu oznacza, że minimalny wspólny element nadrzędny służy jako źródło wiarygodnych danych, które mogą koordynować relacje między wieloma elementami. Wprowadzenie stanu dynamicznego narusza tę zasadę.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Stan i Jetpack Compose
- Listy i siatki
- Kotlin dla Jetpack Compose