Podobnie jak większość innych zestawów narzędzi interfejsu, 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. Funkcja tworzenia jest bardzo podobna, ale na początku ma dodatkową fazę o nazwie kompozycja.
Dokumentacja Compose opisuje kompozycję w artykułach Myślenie w kategoriach Compose i Stan i Jetpack Compose.
Trzy fazy ramki
Proces tworzenia składa się z 3 głównych etapów:
- Kompozycja: który interfejs użytkownika ma się wyświetlać. Compose uruchamia funkcje typu „composable” i tworzy opis interfejsu.
- Układ: gdzie umieścić interfejs. Ten etap składa się z 2 krokó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: sposób renderowania. Elementy interfejsu są rysowane na obiekcie 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, aby utworzyć klatkę (znaną też jako jednokierunkowy przepływ danych). BoxWithConstraints
, LazyColumn
i LazyRow
to ważne wyjątki, w których skład elementów podrzędnych zależy od fazy układu elementu nadrzędnego.
Każda z tych faz występuje w przypadku każdej klatki, ale aby zoptymalizować wydajność, Compose unika powtarzania działań, które w każdej z tych faz dałyby te same wyniki na podstawie tych samych danych wejściowych. Kompozycja pomija uruchamianie funkcji kompozycyjnej, jeśli może ponownie wykorzystać poprzedni wynik, a interfejs Compose UI nie zmienia układu ani nie rysuje ponownie całego drzewa, jeśli nie musi tego robić. Compose wykonuje tylko minimalną ilość pracy potrzebną do zaktualizowania interfejsu. Ta optymalizacja jest możliwa, ponieważ Compose śledzi odczyty stanu w różnych fazach.
Omówienie etapów
W tej sekcji opisujemy bardziej szczegółowo, jak przebiegają 3 fazy Compose w przypadku funkcji kompozycyjnych.
Kompozycja
W fazie kompozycji środowisko wykonawcze Compose wykonuje funkcje typu „composable” i generuje strukturę drzewa reprezentującą interfejs. Drzewo interfejsu składa się z węzłów układu, które zawierają wszystkie informacje potrzebne w kolejnych fazach, jak pokazano na tym filmie:
Rysunek 2. Drzewo reprezentujące interfejs utworzone w fazie kompozycji.
Fragment kodu i drzewa interfejsu wygląda tak:

W tych przykładach każda funkcja kompozycyjna w kodzie jest powiązana z jednym węzłem układu w drzewie interfejsu. W bardziej złożonych przykładach funkcje kompozycyjne mogą zawierać logikę i przepływ sterowania oraz tworzyć różne drzewa w zależności od stanu.
Układ
W fazie układu Compose używa drzewa interfejsu wygenerowanego w fazie kompozycji jako danych wejściowych. Kolekcja 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 interfejsu podczas fazy układu.
W fazie układu drzewo jest przeszukiwane za pomocą tego 3-etapowego algorytmu:
- Mierzenie dzieci: węzeł mierzy swoje dzieci, jeśli istnieją.
- Określanie własnego rozmiaru: na podstawie tych pomiarów węzeł określa własny rozmiar.
- Umieść elementy podrzędne: każdy węzeł podrzędny jest umieszczany względem własnej pozycji węzła.
Na koniec tego etapu każdy węzeł układu ma:
- przypisana szerokość i wysokość;
- Współrzędne x, y miejsca, w którym ma być narysowany
Przypomnij sobie drzewo interfejsu z poprzedniej sekcji:
W przypadku tego drzewa algorytm działa w ten sposób:
Row
mierzy swoje elementy podrzędne,Image
iColumn
.- Mierzona jest wartość
Image
. Nie ma elementów podrzędnych, więc określa własny rozmiar i przesyła go doRow
. - Następnie mierzona jest
Column
. Najpierw mierzy własne elementy podrzędne (2 komponentyText
). - Mierzona jest pierwsza
Text
. Nie ma elementów podrzędnych, więc określa własny rozmiar i przesyła go z powrotem doColumn
.- Mierzona jest druga wartość
Text
. Nie ma żadnych elementów podrzędnych, więc określa własny rozmiar i przekazuje go doColumn
.
- Mierzona jest druga wartość
Column
na podstawie pomiarów dziecka określa swój rozmiar. Wykorzystuje maksymalną szerokość elementu podrzędnego i sumę wysokości elementów podrzędnych.- Element
Column
umieszcza elementy podrzędne względem siebie, jeden pod drugim w pionie. Row
na podstawie pomiarów dziecka określa swój rozmiar. Wykorzystuje maksymalną wysokość elementu podrzędnego i sumę szerokości elementów podrzędnych. Następnie umieszcza elementy podrzędne.
Zwróć uwagę, że każdy węzeł został odwiedzony tylko raz. Środowisko wykonawcze Compose wymaga tylko jednego przejścia przez drzewo interfejsu, 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 przejście zwiększa się liniowo. Jeśli jednak każdy węzeł był odwiedzany wielokrotnie, czas przejścia rośnie wykładniczo.
Rysowanie
W fazie rysowania drzewo jest ponownie przechodzone od góry do dołu, a każdy węzeł jest rysowany na ekranie po kolei.
Rysunek 5. W fazie rysowania piksele są rysowane na ekranie.
Na podstawie poprzedniego przykładu zawartość drzewa jest rysowana w ten sposób:
- Element
Row
rysuje wszelkie treści, które może zawierać, np. kolor tła. Image
rysuje się samo.Column
rysuje się samo.- Pierwszy i drugi znak
Text
rysują się odpowiednio.
Rysunek 6. Drzewo interfejsu i jego reprezentacja graficzna.
Odczyty stanu
Gdy podczas jednej z wymienionych wcześniej faz odczytasz value
snapshot state
, Compose automatycznie śledzi, co robił w momencie odczytania value
. To śledzenie umożliwia ponowne wykonanie czytnika przez Compose, gdy zmieni się stan value
, i jest podstawą obserwacji stanu w Compose.
Stan tworzy się zwykle za pomocą mutableStateOf()
, a następnie uzyskuje się do niego dostęp na 2 sposoby: bezpośrednio przez właściwość value
lub za pomocą delegata właściwości Kotlin. Więcej informacji o nich znajdziesz w artykule Stan w funkcjach kompozycyjnych. Na potrzeby tego przewodnika „odczyt stanu” odnosi się do obu 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 przypadku delegata właściwości do uzyskiwania dostępu do value
i aktualizowania go używane są funkcje „getter” i „setter”. Te funkcje pobierające i ustawiające 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 opisane wcześniej 2 sposoby są równoważne.
Każdy blok kodu, który można ponownie wykonać, gdy zmieni się stan odczytu, to zakres ponownego uruchomienia. Compose śledzi zmiany stanu value
i ponownie uruchamia zakresy
w różnych fazach.
Odczyty stanu fazowego
Jak wspomnieliśmy wcześniej, w Compose są 3 główne fazy, a Compose śledzi, który stan jest odczytywany w każdej z nich. Dzięki temu Compose może powiadamiać tylko te fazy, które muszą wykonać pracę dla każdego elementu interfejsu.
W sekcjach poniżej opisujemy poszczególne fazy i wyjaśniamy, co się dzieje, gdy w ich trakcie 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 stan value
się zmieni, kompozytor ponownie uruchomi wszystkie funkcje kompozycyjne, które odczytują ten stan value
. Pamiętaj, że środowisko wykonawcze może pominąć niektóre lub wszystkie funkcje kompozycyjne, jeśli dane wejściowe nie uległy zmianie. Więcej informacji znajdziesz w sekcji Pomijanie, jeśli dane wejściowe nie uległy zmianie.
W zależności od wyniku kompozycji interfejs Compose UI przeprowadza fazy układu i rysowania. Może pominąć te etapy, jeśli treść pozostanie bez zmian, a rozmiar i układ nie ulegną zmianie.
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) )
Etap 2. Układ
Faza układu składa się z 2 etapów: pomiaru i umieszczania. Krok pomiaru uruchamia funkcję lambda pomiaru przekazaną do funkcji kompozycyjnej Layout
, metody MeasureScope.measure
interfejsu LayoutModifier
i innych.
Krok umieszczania uruchamia blok umieszczania funkcji layout
, blok lambda funkcji Modifier.offset { … }
i podobne funkcje.
Odczytywanie stanu podczas każdego z tych kroków wpływa na układ i potencjalnie na fazę rysowania. Gdy stan value
się zmieni, interfejs Compose UI zaplanuje fazę układu. Jeśli rozmiar lub położenie uległy zmianie, uruchamia 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 podczas rysowania mają wpływ na fazę rysowania. Przykłady to Canvas()
, Modifier.drawBehind
i Modifier.drawWithContent
. Gdy stan value
się zmieni, interfejs Compose wykona 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) }
Optymalizacja odczytów stanu
Kompozycja śledzi odczyty lokalnego stanu, więc możesz zminimalizować ilość pracy wykonywanej przez odczytywanie każdego stanu w odpowiedniej fazie.
Rozważmy ten przykład. W tym przykładzie występuje Image()
, które używa modyfikatora przesunięcia, aby przesunąć swoją ostateczną pozycję 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ść. Zgodnie z zapisem kod odczytuje value
stanu firstVisibleItemScrollOffset
i przekazuje go do funkcji Modifier.offset(offset: Dp)
. Podczas przewijania przez użytkownika firstVisibleItemScrollOffset
value
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 treścią funkcji Box
.
To przykład odczytywania stanu w fazie kompozycji. Niekoniecznie jest to coś złego, a w zasadzie jest to podstawa ponownego komponowania, która umożliwia zmianom danych emitowanie nowego interfejsu.
Kluczowa kwestia: ten przykład jest nieoptymalny, ponieważ każde zdarzenie przewijania powoduje ponowną ocenę, pomiar, rozmieszczenie i wreszcie narysowanie całej treści kompozycyjnej. Faza tworzenia jest wywoływana przy każdym przewijaniu, nawet jeśli wyświetlana treść nie uległa zmianie, a zmieniła się tylko jej pozycja. Możesz zoptymalizować odczyt stanu, aby ponownie wywołać tylko fazę układu.
Przesunięcie za pomocą funkcji lambda
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. Aby użyć kodu, zaktualizuj go:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Dlaczego ta metoda jest bardziej wydajna? Blok lambda przekazywany do modyfikatora jest wywoływany podczas fazy układu (a konkretnie podczas kroku umieszczania w tej fazie), co oznacza, że stan firstVisibleItemScrollOffset
nie jest już odczytywany podczas kompozycji. Ponieważ Compose śledzi, kiedy stan jest odczytywany, ta zmiana oznacza, że jeśli firstVisibleItemScrollOffset
value
ulegnie zmianie, Compose musi tylko ponownie uruchomić fazy układu i rysowania.
Oczywiście odczytywanie stanów w fazie kompozycji jest często absolutnie konieczne. Niemniej jednak w niektórych przypadkach możesz zminimalizować liczbę ponownych kompozycji, filtrując zmiany stanu. Więcej informacji znajdziesz w artykule derivedStateOf
: przekształcanie jednego lub kilku obiektów stanu w inny stan.
Pętla ponownego komponowania (zależność cykliczna fazy)
W tym przewodniku wspominaliśmy wcześniej, że fazy Compose są zawsze wywoływane w tej samej kolejności i że w ramach tej samej klatki nie można się cofnąć. Nie uniemożliwia to jednak aplikacjom wchodzenia w pętle kompozycji w różnych klatkach. Przeanalizuj ten przykład:
Box { var imageHeightPx by remember { mutableStateOf(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() } ) ) }
W tym przykładzie zastosowano kolumnę pionową, w której obraz znajduje się u góry, a tekst pod nim. Używa parametru Modifier.onSizeChanged()
, aby uzyskać rozmiar obrazu, a następnie parametru Modifier.padding()
na tekście, aby przesunąć go 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 dociera do „końcowego” układu w ramach jednej klatki. Kod opiera się na wielu klatkach, co powoduje niepotrzebną pracę i sprawia, że interfejs użytkownika przeskakuje na ekranie.
Kompozycja pierwszej klatki
Podczas fazy kompozycji pierwszej klatki wartość imageHeightPx
jest początkowo 0
. W efekcie kod zapewnia tekst z Modifier.padding(top = 0)
.
W kolejnej fazie układu wywoływane jest wywołanie zwrotne modyfikatora onSizeChanged
, które aktualizuje wartość imageHeightPx
do rzeczywistej wysokości obrazu. Funkcja Compose, a następnie planuje ponowne komponowanie na potrzeby następnej klatki. Jednak w bieżącej fazie rysowania tekst jest renderowany z wypełnieniem 0
, ponieważ zaktualizowana wartość imageHeightPx
nie jest jeszcze odzwierciedlona.
Kompozycja drugiej klatki
Funkcja Compose inicjuje drugą klatkę, co jest wywoływane przez zmianę wartości imageHeightPx
. W fazie kompozycji tej ramki stan jest odczytywany w Box
bloku treści. Tekst jest teraz uzupełniony o odstępy, które dokładnie odpowiadają wysokości obrazu. Podczas fazy układu wartość imageHeightPx
jest ponownie ustawiana, ale nie jest planowana dalsza rekompozycja, ponieważ wartość pozostaje spójna.
Ten przykład może wydawać się naciągany, ale uważaj na ten ogólny wzorzec:
Modifier.onSizeChanged()
,onGloballyPositioned()
lub inne operacje układu.- Aktualizowanie stanu
- Użyj tego stanu jako danych wejściowych modyfikatora układu (
padding()
,height()
lub podobnego). - Potencjalnie powtarzające się
Rozwiązaniem w przypadku powyższego przykładu jest użycie odpowiednich elementów układu. Powyższy przykład można zaimplementować za pomocą elementu Column()
, ale możesz mieć bardziej złożony przykład, który wymaga niestandardowego rozwiązania, co będzie wymagać napisania niestandardowego układu. Więcej informacji znajdziesz w przewodniku Układy niestandardowe.
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 układu lub utworzenie niestandardowego układu oznacza, że minimalny wspólny element nadrzędny służy jako źródło informacji, które może koordynować relacje między wieloma elementami. Wprowadzenie stanu dynamicznego narusza tę zasadę.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Stan i Jetpack Compose
- Listy i siatki
- Kotlin w Jetpack Compose