Proces tworzenia kompozycji przebiega w 3 ściśle uporządkowanych fazach:
- Kompozycja: uruchamia funkcje
@Composable, aby tworzyć i aktualizować drzewo interfejsu. - Układ: mierzy elementy podrzędne, a następnie je umieszcza.
- Rysowanie: wysyła polecenia rysowania na płótnie, aby renderować piksele na ekranie.
Gdy podczas dowolnej fazy nastąpi odczyt funkcji Compose State, Compose automatycznie rejestruje zależność między tym stanem a odpowiednią fazą.
Co sprawia, że zapis jest „odwrotny”?
Zapis wsteczny występuje, gdy stan jest modyfikowany w późniejszej fazie (lub w zakresie podrzędnym – czyli w zakresie funkcji kompozycyjnej wykonywanym później w kolejności w ramach tego samego przebiegu kompozycji) niż w miejscu, w którym został odczytany. Wymusza to ponowne utworzenie kompozycji przez Compose poprzez zaplanowanie ponownego uruchomienia wcześniejszej fazy lub funkcji kompozycyjnej. Zapisywanie wsteczne to niezoptymalizowana pętla rekompozycji.
Konsekwencje zapisów wstecznych
Zapisywanie wsteczne nie jest koniecznie czymś złym i nie zawsze powoduje awarie, ale jest nieefektywne i może na kilka sposobów pogorszyć wydajność aplikacji:
- Dodatkowe renderowanie klatek i pomijanie klatek: zapis wsteczny wymusza na Compose wykonywanie zbędnych przebiegów kompozycji w kolejnych klatkach, co powoduje marnowanie zasobów procesora i GPU oraz może powodować zacinanie się animacji.
- 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. o zerowym rozmiarze lub z nieprawidłowym przesunięciem). Powoduje to widoczne skoki lub migotanie układu, gdy renderowana jest druga klatka.
- Nieskończone pętle ponownego komponowania: jeśli zmiana stanu zmienia rozmiar układu, a rozmiar układu ciągle zapisuje nową wartość w stanie, możesz stworzyć nieskończoną pętlę klatek, w której ekran ciągle ponownie komponuje każdą klatkę, nie stabilizując się.
Przechodzenie do przodu przez etapy
Zmiany stanu powinny zawsze przebiegać w kierunku kolejnych etapów:
| Faza odczytu | Write Context | Dozwolone? | Dlaczego |
|---|---|---|---|
Układ (Modifier.offset { }) |
Kompozycja | Tak | Kompozycja aktualizuje stan → układ odczytuje go później w tej samej ramce bez ponownego komponowania. |
Remis (graphicsLayer { }, drawBehind { }) |
Kompozycja | Tak | Kompozycja aktualizuje stan → rysowanie odczytuje go w ostatniej fazie. Kompozycja i układ są całkowicie pomijane. |
| Rysuj | Układ | Tak | Układ aktualizuje stan, a funkcja rysowania go odczytuje. Jest to prawidłowy przepływ. |
| Kompozycja | Wywołanie zwrotne zdarzenia (onClick, onValueChange) powodujące zmianę stanu jazdy. Uwaga: wywołania zwrotne układu nie są traktowane jako zdarzenia. |
Tak | Zdarzenie zmienia stan, który jest używany do tworzenia kompozycji. Jeśli zdarzenie występuje poza ramką (nie w sekcji Kompozycja, Układ ani Rysowanie), jest to prawidłowe. |
| Kompozycja | Współprogram (LaunchedEffect) |
Tak – 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 warstwy stanu. Należy ich unikać, jeśli to możliwe. |
| Miejsce docelowe (w układzie) | Pomiary (w układzie) | Tak | W przypadku układu dopuszczalne jest zaktualizowanie stanu, a następnie odczytanie go w miejscu docelowym. |
| Pomiary (w układzie) | Miejsce docelowe (w układzie) | Nie – pisanie od tyłu | Zapisywanie stanu umieszczenia, który następnie jest odczytywany, powoduje pętlę ponownego pomiaru. |
| Kompozycja | Układ (onSizeChanged, LayoutModifier) |
Nie – pisanie od prawej do lewej | Układ unieważnia kompozycję → pętla ponownej kompozycji. |
| Kompozycja | Remis (drawWithContent, Canvas) |
Nie – pisanie od tyłu | Rysowanie unieważnia kompozycję → pętla ponownej kompozycji. |
Kombinacje faz: wsteczna i do przodu
Poniżej znajdziesz przykłady pisania od prawej do lewej w Compose i sposoby ich rozwiązania.
Wstecz: czytanie w widoku Kompozycja, pisanie w widoku Układ
- Co się dzieje: kompozycja odczytuje
componentHeight, aby określić, który interfejs użytkownika ma zostać wyemitowany. W dalszej części ramki faza układu mierzy lub umieszcza widoki i zapisuje nową wartość wcomponentHeight(np. za pomocąonSizeChanged,onGloballyPositionedlub niestandardowegoLayoutModifier). - Wynik: zmiana
componentHeightw układzie unieważnia właśnie ukończony etap kompozycji. Pamiętaj, żeonSizeChangedraporty rozmiar po zakończeniu pomiaru układu. Jeśli zaktualizowana wartość stanu ustabilizuje się w następnym przebiegu, rekompozycja może się zatrzymać po jednej dodatkowej klatce. Jeśli jednak nowa wartość nadal będzie zmieniać rozmiar, spowoduje to nieskończoną pętlę klatek. PonadtoonGloballyPositionedjest wykonywana po układzie i umiejscowieniu, co sprawia, że zapisywanie stanu w jej wnętrzu jest jeszcze bardziej podatne na ciągłe pętle rekompozycji i ponownego układania 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: czytanie w kompozycji, pisanie w rysunku
- Co się dzieje: stan jest odczytywany w treści funkcji Composable (faza kompozycji), ale zmieniany w funkcjach
Modifier.drawWithContent,Modifier.drawBehindlubCanvas(faza rysowania). - Wynik: faza rysowania zmienia stan → kompozycja unieważniona → nieskończona pętla.
// ❌ 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: czytanie w trybie kompozycji, pisanie w trybie kompozycji (ta sama faza)
- Co się dzieje: odczytanie wartości
countw funkcji typu „composable” i zmodyfikowanie jejcountbezpośrednio w innym slocie treści funkcji kompozycyjnej po odczytaniu. - Wynik: system migawek rejestruje odczyt i kolejny zapis w ramach tej samej kompozycji, 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 zapisywaniu wstecznemu
- Nie pisz w stanie
onGloballyPositioned,onSizeChangedaniLayoutModifier, jeśli ten stan jest odczytywany w kompozycji, ponieważ powoduje to problem z poprawnością pierwszej klatki.- Jeśli współrzędne lub rozmiary układu są potrzebne tylko do rysowania niestandardowego, odczytuj je bezpośrednio w fazie rysowania lub układu (np. za pomocą
Modifier.drawWithCachelubModifier.layout). - W przypadku określania rozmiaru na poziomie okna (
WindowWidthSizeClass): przenieś obserwację rozmiaru podnośnika na poziom okna. Kompozycja rozgałęzia się na klasy rozmiaru okna przed pomiarem lokalnym. - Zachowaj jednolitość kompozycji: używaj jednego niestandardowego układu lub komponentów, takich jak
FlowRowlubLazyVerticalGrid, które dostosowują pomiary i umiejscowienie w fazie 2 bez zmiany kompozycji ani stanu używanego w kompozycji. - Użyj podkompozycji: użyj
BoxWithConstraintslubSubcomposeLayout, gdy kompozycje 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łowość pierwszej klatki, zapisując rozmiar w
onSizeChanged, aby wywołać ponowne złożenie 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 rysowania niestandardowego, odczytuj je bezpośrednio w fazie rysowania lub układu (np. za pomocą
- Nie zmieniaj stanu po pierwszym odczytaniu w kompozycji:
- Chociaż podczas tworzenia wiadomości możesz bezpiecznie zapisywać obiekty
MutableStatepozaSideEffect, zachowaj szczególną ostrożność, aby nie zapisywać stanu, który mógł zostać wcześniej odczytany podczas tworzenia wiadomości. Zalecamy używanierememberUpdatedState, gdy zajdzie potrzeba zapisania stanu w kompozycji. Zapisywanie stanu podczas kompozycji w inny sposób jest zwykle oznaką braku efektu lub nieodpowiednio zaprojektowanego stanu lub komponentu. Pamiętaj, że kompozycja jest optymistyczna i zawsze wykonuje się z najnowszą wartością stanu, więc w ponownych kompozycjach możesz nie widzieć wszystkich zmian stanu. Aktualizacje interfejsu nie powinny być używane do obsługi jednorazowych zdarzeń, dlatego rzadko zdarza się, aby wartość stanu wymagała aktualizacji w wyniku ponownego komponowania. - Unikaj modyfikowania stanów obserwowanych poza kompozycją (np. pól
ViewModellub flagisVisible). Stan, który wpływa na kompozycję, powinien być zapisywany w funkcjach lambda zdarzeń (onClick), korutynach (LaunchedEffect) lub efektach ubocznych (SideEffect). Wyjątkiem jestrememberUpdatedState, ponieważ jest on przeznaczony do zmiany stanu, który jest używany tylko w treści@Composable.
- Chociaż podczas tworzenia wiadomości możesz bezpiecznie zapisywać obiekty
- Odłóż odczyty stanu do ostatniej możliwej fazy:
- Odczytywanie stanów w programie Draw (
Modifier.graphicsLayer { alpha = ... }) lub Layout (Modifier.offset { IntOffset(...) }) zapewnia, że zmiany unieważniają tylko fazę 2 lub 3, całkowicie pomijając fazę 1 (kompozycja).
- Odczytywanie stanów w programie Draw (