0Pricing
Android Academy · Lekcja

Poskramianie rekompozycji

Stabilne parametry i mniej rekompozycji

Poskramianie rekompozycji to bezpłatna lekcja Android Academy na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Android Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Android Academy zawiera 4 lekcji w sumie.

Rekompozycja: pokrętło wydajności Compose

W Jetpack Compose interfejs jest opisywany za pomocą funkcji. Gdy zmienia się stan, Compose wykonuje rekompozycję: ponownie uruchamia te composable, które odczytują ten stan, aby zaktualizować ekran.

Rekompozycja jest czymś normalnym i niedrogim, jeśli jej zakres jest dobrze ograniczony. Jednak gdy zachodzi zbyt często lub obejmuje zbyt dużą część drzewa, staje się główną przyczyną zacinania w Compose. W tej lekcji nauczy się Pan ograniczać rekompozycję, aby była mała i rzadka.

Wyświetlanie liczby rekompozycji

Zanim zacznie Pan coś naprawiać, należy wykonać pomiar. Layout Inspector w Android Studio pokazuje w czasie rzeczywistym liczbę rekompozycji dla każdego composable. Można także użyć metryk kompilatora Compose lub prostego licznika debug.

Prosty sposób polega na tym, że SideEffect zwiększa wartość referencji za każdym razem, gdy composable przechodzi rekompozycję, dzięki czemu podczas tworzenia aplikacji można rejestrować nieoczekiwane przypadki.

@Composable
fun RecompositionCounter(tag: String) {
    val count = remember { mutableStateOf(0) }
    SideEffect { count.value++ }
    Log.d("Recompose", "$tag recomposed ${count.value} times")
}

// Drop RecompositionCounter("PriceLabel") inside a composable
// to watch how often it re-runs while you interact.

Odczytuj stan tak późno, jak to możliwe

Compose wykonuje rekompozycję tylko tych composable, które odczytują wartość stanu. Jeśli stan odczytuje rodzic, rekompozycję przechodzi cały rodzic; jeśli odczytuje go tylko małe dziecko, rekompozycję przechodzi wyłącznie ono.

Dlatego należy przesuwać odczyty stanu w dół drzewa. W poniższej złej wersji cała funkcja Column przechodzi rekompozycję przy każdym takcie, a dobra wersja ogranicza ją do etykiety.

// BAD: Column reads `seconds`, so everything recomposes each second
@Composable
fun TimerBad(seconds: Int) {
    Column {
        ExpensiveHeader()
        Text("Elapsed: $seconds")
    }
}

// GOOD: only the Text reads the value via a lambda
@Composable
fun TimerGood(seconds: () -> Int) {
    Column {
        ExpensiveHeader()
        Text("Elapsed: ${seconds()}")
    }
}

Odrocz odczyty za pomocą lambd

Skuteczny wzorzec polega na tym, aby zamiast często zmieniającej się wartości przekazywać lambdę, która ją zwraca. Jedynym composable, które przechodzi rekompozycję, jest wtedy to, które ostatecznie wywołuje lambdę.

Dlatego Modifier.offset { ... } i graphicsLayer { ... } przyjmują lambdy: wartości przewijania i animacji zmieniają się w każdej klatce, a lambda całkowicie wyklucza rekompozycję z fazy układu.

// Passing the value: parent recomposes every frame of scroll
Box(Modifier.offset(y = scrollOffset.dp))

// Passing a lambda: skips recomposition, updates in the layout phase
Box(Modifier.offset { IntOffset(x = 0, y = scrollOffset.roundToInt()) })

// Same idea for alpha/scale during animation:
Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier.graphicsLayer { alpha = animatedAlpha() }
)

Stabilność: dlaczego Compose pomija rekompozycję

Compose może pominąć rekompozycję composable, jeśli wszystkie jego parametry są stabilne i niezmienione. Typ jest stabilny, gdy Compose może zaufać, że equals odzwierciedla rzeczywiste zmiany, a jego publiczne pola nie zmieniają się niezauważenie.

  • Stabilne: typy prymitywne, String, niezmienne klasy danych, State.
  • Niestabilne: interfejsy List/Map, klasy z polami var, typy z modułów bez kompilatora.

Niestabilny parametr wymusza rekompozycję, nawet gdy nic się nie zmieniło.

// UNSTABLE: List is an interface; Compose cannot assume immutability,
// so UserList recomposes even if the contents are identical.
@Composable
fun UserList(users: List<User>) { /* ... */ }

data class User(val id: Long, val name: String) // stable: all vals

Stabilizuj parametry

Dwa typowe sposoby naprawy niestabilnych parametrów:

  • Należy używać niezmiennych kolekcji z kotlinx.collections.immutable (np. ImmutableList), które Compose traktuje jako stabilne.
  • Klasy, nad którą ma Pan kontrolę, można oznaczyć adnotacją @Immutable lub @Stable, obiecując Compose, że nie będzie się ona zmieniać.

Dzięki temu Compose może bezpiecznie pominąć rekompozycję, gdy ponownie przekazana zostanie ta sama instancja.

import kotlinx.collections.immutable.ImmutableList
import androidx.compose.runtime.Immutable

@Immutable
data class UiState(
    val title: String,
    val users: ImmutableList<User>
)

// Stable parameter -> Compose can skip this when state is unchanged
@Composable
fun UserList(users: ImmutableList<User>) { /* ... */ }

remember: nie przeliczaj wszystkiego w każdej klatce

Composable mogą być uruchamiane wiele razy. Każde nietrywialne obliczenie wykonane bezpośrednio w treści composable uruchamia się przy każdej rekompozycji. Należy opakować je w remember, aby było przeliczane tylko wtedy, gdy zmienią się jego klucze.

W przypadku wartości wyprowadzanych z innego stanu warto użyć derivedStateOf, które emituje wynik ponownie tylko wtedy, gdy rzeczywiście się on zmieni.

// Recomputes the sorted list only when `items` changes
val sorted = remember(items) { items.sortedBy { it.name } }

// derivedStateOf: only triggers readers when the BOOLEAN flips,
// not on every scroll pixel
val showButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
if (showButton) ScrollToTopButton()

Stabilne klucze na leniwych listach

W LazyColumn/LazyRow należy nadać każdemu elementowi stabilny klucz. Bez kluczy wstawianie lub zmiana kolejności elementów zmusza Compose do ponownej kompozycji i ponownego pomiaru elementów, które w rzeczywistości się nie zmieniły, ponieważ są one śledzone według pozycji.

Stabilny klucz pozwala Compose dopasować elementy między aktualizacjami i pominąć te, które się nie zmieniły.

LazyColumn {
    items(
        items = users,
        key = { user -> user.id }   // stable identity
    ) { user ->
        UserRow(user)
    }
}
// Now adding a user at the top reuses existing rows
// instead of recomposing the whole list.

Podnoś stan, przekazuj zdarzenia w górę

Niestabilne parametry typu lambda również mogą uniemożliwić pomijanie rekompozycji, jeśli przy każdej rekompozycji tworzona jest nowa instancja lambdy. Należy stabilizować wywołania zwrotne, zapamiętując je lub odwołując się do stabilnych funkcji.

W połączeniu z podnoszeniem stanu (stan znajduje się u wywołującego, a zdarzenia przepływają w górę) pozwala to utrzymać composable liści jako tanie w wykonaniu i możliwe do pomijania.

@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
    TextField(value = query, onValueChange = onQueryChange)
}

// In the caller, a remembered lambda keeps the reference stable:
val onChange = remember { { newValue: String -> viewModel.setQuery(newValue) } }
SearchBar(query = query, onQueryChange = onChange)

Unikaj zbyt wysokiego odczytywania stanu przewijania i animacji

Klasyczny błąd polega na odczytywaniu szybko zmieniającego się stanu (przesunięcia przewijania, postępu animacji) w composable wysokiego poziomu. Zmusza to całe poddrzewo do przechodzenia rekompozycji w każdej klatce.

Takie odczyty należy umieszczać w lambdach Modifier (offset {}, graphicsLayer {}, drawBehind {}), aby praca odbywała się w fazie układu lub rysowania, a nie rekompozycji. To właśnie decyduje o różnicy między płynnym przewijaniem 60 kl./s a przewijaniem pełnym przycięć.

// Animated color used only for drawing -> stay in the draw phase
Box(
    Modifier.drawBehind {
        drawRect(color = animatedColor())  // lambda read, no recomposition
    }
)

Lista kontrolna rekompozycji

Gdy ekran wydaje się zacinać podczas interakcji, należy przejrzeć tę listę:

  • Czy parametry są stabilne (niezmienne dane, ImmutableList)?
  • Czy szybko zmieniający się stan jest odczytywany tak nisko, jak to możliwe, najlepiej w lambdach modyfikatorów?
  • Czy leniwe listy mają stabilne klucze?
  • Czy kosztowne obliczenia są umieszczone za remember / derivedStateOf?
  • Czy Layout Inspector potwierdził faktyczne zmniejszenie liczby rekompozycji?

Każde zaznaczone pole usuwa zbędną pracę z każdej klatki.

Szybkie sprawdzenie

Przekazuje Pan List<User> do composable i zauważa, że przechodzi ono rekompozycję, nawet gdy dane się nie zmieniły. Jakie jest najbardziej bezpośrednie rozwiązanie?

Podsumowanie: mniejsza i rzadsza rekompozycja

Nauczył się Pan ograniczać rekompozycję, czyli najważniejszy problem wydajności Compose:

  • Należy mierzyć liczbę rekompozycji za pomocą Layout Inspector lub licznika debug.
  • Należy odczytywać stan tak nisko, jak to możliwe i odraczać odczyty za pomocą lambd.
  • Należy zapewniać parametrom stabilność za pomocą niezmiennych danych oraz @Immutable/ImmutableList.
  • Należy używać remember i derivedStateOf, aby unikać przeliczania wartości w każdej klatce.
  • Należy nadawać elementom leniwych list stabilne klucze i umieszczać szybkie odczyty stanu w lambdach modyfikatorów.

Teraz przechodzimy od procesora i rekompozycji do pamięci: wyszukiwania i naprawiania wycieków.

Często zadawane pytania

Czy lekcja „Poskramianie rekompozycji” jest bezpłatna?

Tak — pełny tekst „Poskramianie rekompozycji” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Android Academy, przejdź na CoddyKit PRO. Kurs Android Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Poskramianie rekompozycji”?

Stabilne parametry i mniej rekompozycji Ćwiczysz Android Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Android Academy?

Nie wymagamy żadnego doświadczenia. Android Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „Poskramianie rekompozycji”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Android Academy?

Tak. Każda lekcja Android Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Pomiar wydajności
  2. Poskramianie rekompozycji
  3. Wycieki pamięci i ich usuwanie
  4. Profile uruchamiania i Baseline Profiles
← Powrót do Android Academy