Compose wykonuje ramkę w 3 ściśle uporządkowanych, następujących po sobie fazach:
- Kompozycja: uruchamia funkcje
@Composable, aby utworzyć i zaktualizować drzewo interfejsu. - Układ: mierzy elementy podrzędne, a następnie je umieszcza.
- Rysowanie: wysyła polecenia rysowania na obszarze roboczym, aby renderować piksele na ekranie.
Za każdym razem, gdy State Compose jest odczytywany na dowolnym etapie, Compose automatycznie rejestruje zależność między tym stanem a odpowiednim etapem.
Co sprawia, że zapis jest „wsteczny”?
Zapis wsteczny występuje, gdy stan jest modyfikowany w późniejszej fazie (lub w zakresie podrzędnym, czyli w zakresie funkcji kompozycyjnej wykonywanej później w ramach tego samego przebiegu tworzenia) niż faza, w której został odczytany. Wymusza to na Compose ponowne utworzenie przez zaplanowanie wcześniejszej fazy lub funkcji kompozycyjnej do ponownego uruchomienia. Zapis wsteczny to niezoptymalizowana pętla rekompozycji.
Konsekwencje zapisów wstecznych
Zapisywanie wstecz nie zawsze jest złe i nie zawsze powoduje awarie, ale jest nieefektywne i może na kilka sposobów negatywnie wpływać na wydajność aplikacji:
- Renderowanie dodatkowych klatek i pomijanie klatek: zapis wsteczny wymusza na Compose wykonywanie nadmiarowych przebiegów kompozycji w kolejnych klatkach, co marnuje zasoby procesora i GPU oraz może powodować zacinanie się.
- Problemy z poprawnością pierwszej klatki: jeśli komponent wymaga zapisu wstecznego, aby określić ostateczne wymiary lub stan, pierwsza klatka jest renderowana z nieprawidłowymi, domyślnymi lub nieustalonymi danymi (np. z rozmiarem 0 lub nieprawidłowym przesunięciem). Powoduje to widoczne migotanie obrazu lub układu podczas renderowania drugiej klatki.
- Nieskończone pętle rekompozycji: jeśli zmiana stanu wpływa na rozmiar układu, a rozmiar układu stale zapisuje nową wartość w stanie, może dojść do utworzenia nieskończonej pętli klatek, w której ekran stale rekomponuje każdą klatkę bez stabilizacji.
Przekierowanie przepływu przez fazy
Zmiany stanu powinny zawsze przebiegać do przodu przez te fazy:
| Faza odczytu | Kontekst zapisu | Dopuszczalne? | Dlaczego |
|---|---|---|---|
Układ (Modifier.offset { }) |
Skład demograficzny | Tak | Kompozycja aktualizuje stan → Układ odczytuje go później w tej samej ramce bez ponownego komponowania. |
Rysowanie (graphicsLayer { }, drawBehind { }) |
Skład demograficzny | Tak | Kompozycja aktualizuje stan → funkcja Draw odczytuje go w ostatniej fazie. Kompozycja i układ są całkowicie pomijane. |
| Rysowanie | Układ | Tak | Układ aktualizuje stan, a funkcja rysowania go odczytuje, co jest prawidłowym przepływem. |
| Skład demograficzny | Wywołanie zwrotne zdarzenia (onClick, onValueChange) powodujące zmianę stanu jazdy. Uwaga: wywołania zwrotne układu nie są liczone jako zdarzenia. |
Tak | Zdarzenie zmienia stan, który jest używany do tworzenia kompozycji. Jeśli zdarzenie występuje poza ramką (nie w ramach kompozycji, układu ani rysowania), jest to prawidłowe. |
| Skład demograficzny | Współprogram (LaunchedEffect) |
Tak, ale z zachowaniem ostrożności | Asynchronicznie aktualizuje stan w odpowiedzi na cykl życia lub zdarzenia. Zapisy z efektów mogą być prawidłowe, ale mogą wskazywać na nieefektywne nakładanie stanów. W miarę możliwości należy ich unikać. |
| Miejsce docelowe (w układzie) | Measure (In Layout) | Tak | W Layout aktualizowanie stanu, a następnie odczytywanie go w miejscu docelowym jest dopuszczalne. |
| Measure (In Layout) | Miejsce docelowe (w układzie) | Nie – zapis od tyłu | Zapisywanie stanu w miejscu docelowym, który jest następnie odczytywany, powoduje pętlę ponownego pomiaru. |
| Skład demograficzny | Układ (onSizeChanged, LayoutModifier) |
Nie – Zapis od tyłu | Układ unieważnia kompozycję → pętla rekompozycji. |
| Skład demograficzny | Rysowanie (drawWithContent, Canvas) |
Nie – zapis od tyłu | Pętla Draw invalidates Composition → Recomposition. |
Kombinacje faz: wsteczna i postępująca
Poniżej znajdziesz przykłady zapisu wstecznego w Compose i sposoby ich rozwiązywania.
Wstecz: odczyt w kompozycji, zapis w układzie
- Co się dzieje: kompozycja odczytuje
componentHeight, aby określić, jaki interfejs ma wygenerować. W późniejszej części klatki faza układu mierzy lub umieszcza widoki i zapisuje nową wartość wcomponentHeight(np. za pomocąonSizeChanged,onGloballyPositionedlub niestandardowegoLayoutModifier). - Wynik: zmodyfikowanie elementu
componentHeightw układzie unieważnia fazę kompozycji, która właśnie została zakończona. Pamiętaj, żeonSizeChangedzgłasza rozmiar po zakończeniu pomiaru układu. Jeśli zaktualizowana wartość stanu ustabilizuje się przy następnym przejściu, rekompozycja może zostać zatrzymana po 1 dodatkowej klatce. Jeśli jednak nowa wartość będzie nadal zmieniać rozmiar, spowoduje to nieskończoną pętlę klatek. Co więcej,onGloballyPositioneddziała po układzie i umieszczeniu, przez co zapisy stanu w nim są jeszcze bardziej podatne na ciągłe pętle rekompozycji i zmiany układu w kolejnych klatkach.
// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged) @Composable fun BadAspectRatioImage(painter: Painter) { var calculatedHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current // State read during COMPOSITION: Image( painter = painter, contentDescription = "Dynamic Image", modifier = Modifier .fillMaxWidth() .height(calculatedHeight) .onSizeChanged { size -> // State write during LAYOUT phase! // Triggers backwards write and recomposition pass val aspectRatio = 16f / 9f val widthDp = with(density) { size.width.toDp() } calculatedHeight = widthDp / aspectRatio } ) } // ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition @Composable fun GoodAspectRatioImage( painter: Painter, aspectRatio: Float = 16f / 9f, modifier: Modifier = Modifier ) { Layout( content = { Image( painter = painter, contentDescription = "Dynamic Image" ) }, modifier = modifier ) { measurables, constraints -> val width = constraints.maxWidth val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio() val imageConstraints = constraints.copy( minWidth = width, maxWidth = width, minHeight = height, maxHeight = height ) val placeable = measurables.first().measure(imageConstraints) layout(width, height) { placeable.placeRelative(0, 0) } } }
Wstecz: odczyt w Composition, zapis w Drawing
- Co się dzieje: stan jest odczytywany w treści funkcji kompozycyjnej (faza kompozycji), ale mutowany wewnątrz
Modifier.drawWithContent,Modifier.drawBehindlubCanvas(faza rysowania). - Wynik: faza rysowania zmienia stan → kompozycja zostaje unieważniona → pętla nieskończona.
// ❌ BAD: Read in Composition, Written in Draw () @Composable fun BadBackwardsWriteDraw() { var componentHeight by remember { mutableStateOf(0.dp) } // State read during COMPOSITION: Text( text = "Height is: $componentHeight", modifier = Modifier.drawBehind { // State write during the DRAW phase! // Invalidates Composition -> triggers recomposition loop! componentHeight = size.height.dp } ) }
Wstecz: odczyt w Composition, zapis w Composition (ta sama faza)
- Co się dzieje: odczyt
countw funkcji typu „composable” i modyfikowaniecountbezpośrednio w innym slocie treści funkcji typu „composable” po odczytaniu. - Wynik: system migawek rejestruje odczyt i późniejszy zapis w ramach tego samego przebiegu tworzenia, natychmiast unieważniając bieżący zakres.
// ❌ BAD: Direct write in Composable body after read @Composable fun BadCounter() { var count by remember { mutableIntStateOf(0) } Text("Count: $count") // State read in Composition Button(onClick = {}) { count++ // State write in Composition (Backwards write!) } } // Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read @Composable fun OkCounter() { var count by remember { mutableIntStateOf(0) } Button(onClick = {}) { count++ // State write in Composition } Text("Count: $count") // State read in Composition }
Kluczowe reguły zapobiegające zapisom wstecz
- Nie zapisuj stanu w
onGloballyPositioned,onSizeChanged,onPlaced,onLayoutRectChangedaniLayoutModifier, jeśli ten stan jest odczytywany w Composition, ponieważ powoduje to problem z poprawnością pierwszej klatki.- Jeśli współrzędne lub rozmiary układu są potrzebne tylko do niestandardowego rysowania, odczytaj je bezpośrednio w fazie rysowania lub układu (np. za pomocą
Modifier.drawWithCachelubModifier.layout). - W przypadku rozmiaru na poziomie okna (
WindowWidthSizeClass): przenieś obserwację rozmiaru na poziom okna. Kompozycja rozgałęzia się w zależności od rozmiaru okna, zanim nastąpi pomiar lokalny. - Zachowaj jednolitość kompozycji: używaj jednego niestandardowego układu lub komponentów takich jak
FlowRowlubLazyVerticalGrid, które dostosowują pomiar i umiejscowienie podczas fazy 2 bez ponownego tworzenia lub zmieniania stanu używanego w kompozycji. - Używaj podkompozycji: używaj
BoxWithConstraintslubSubcomposeLayout, gdy elementy kompozycyjne podrzędne muszą się rozgałęziać na podstawie lokalnej szerokości lub wysokości. Zachowaj ostrożność: podkompozycja wiąże się z kosztem wydajności i zwykle można jej uniknąć. - W ostateczności: zezwól na nieprawidłową pierwszą klatkę, zapisując rozmiar w
onSizeChanged, aby wywołać rekompozycję drugiej klatki. Powoduje to widoczne przeskakiwanie układu, zacinanie się i ryzyko nieskończonych pętli.
- Jeśli współrzędne lub rozmiary układu są potrzebne tylko do niestandardowego rysowania, odczytaj je bezpośrednio w fazie rysowania lub układu (np. za pomocą
- Nie zmieniaj stanu po pierwszym odczytaniu w kompozycji:
- Chociaż możesz bezpiecznie zapisywać obiekty
MutableStatepodczas tworzenia kompozycji pozaSideEffect, zachowaj szczególną ostrożność, aby nie zapisywać stanu, który został wcześniej odczytany w kompozycji. Gdy pojawi się potrzeba zapisu stanu w kompozycji, zalecamy użycierememberUpdatedState. Zapisywanie stanu podczas tworzenia kompozycji w inny sposób jest zwykle oznaką braku efektu lub nieprawidłowo zaprojektowanego stanu lub komponentu. Pamiętaj, że kompozycja jest optymistyczna i zawsze wykonuje się z najnowszą wartością stanu, więc możesz nie zobaczyć wszystkich zmian stanu w rekompozycjach. Aktualizacje interfejsu nie powinny być używane do obsługi zdarzeń jednorazowych, co sprawia, że wartość stanu rzadko wymaga aktualizacji w wyniku rekompozycji. - Unikaj mutowania stanów, które są obserwowane poza kompozycją (np. pól
ViewModellub flagisVisible). Zapisy stanu, które wpływają na kompozycję, należą do funkcji lambda zdarzeń (onClick), współprogramów (LaunchedEffect) lub efektów ubocznych (SideEffect).rememberUpdatedStatejest wyjątkiem, ponieważ służy do mutowania stanu, który jest używany tylko w treści@Composable.
- Chociaż możesz bezpiecznie zapisywać obiekty
- Odrocz odczyty stanu do najpóźniejszej możliwej fazy:
- Odczytywanie stanów w fazie rysowania (
Modifier.graphicsLayer { alpha = ... }) lub układu (Modifier.offset { IntOffset(...) }) sprawia, że zmiany unieważniają tylko fazę 2 lub 3, całkowicie pomijając fazę 1 (kompozycja).
- Odczytywanie stanów w fazie rysowania (