Recomposition — какво е, преизграждане на UI при промяна на състоянието

Автор: IT Sectr Публикувано: 2026-06-27 Време за четене: 7 мин

Recomposition е механизъм на Jetpack Compose, който автоматично преизгражда части от потребителския интерфейс при промяна на данни, без ръчно обновяване на View елементите. Когато променлива на състоянието, от която зависи Composable функция, промени стойността си, Compose рестартира само тази функция, оставяйки останалата част от UI дървото незасегната. Според данни на Google Android Developers, 2026, правилното разбиране на Recomposition позволява намаляване на броя на излишните прерисувания с 40–60%.

Основни точки

  • Recomposition — рестартиране на Composable функции при промяна на входните им данни или State
  • Skipping — пропускане на функции, чиито параметри не са се променили (сравнение чрез equals)
  • Stability определя дали Compose може да пропусне функция — стабилните типове се сравняват коректно
  • Smart Recomposition рестартира само минималния набор от функции, а не цялото дърво
  • Рекомпозиция не гарантира прерисуване — Layout и Drawing могат да пропуснат фазата

Какво е Recomposition в Jetpack Compose

Recomposition е повторно изпълнение на Composable функции, които вече са участвали в Composition, с нови стойности на параметри или състояние. Основната цел на рекомпозицията е синхронизиране на UI дървото с актуални данни без преизграждане на целия интерфейс от нулата. За разлика от Composition, който се случва еднократно, Recomposition може да се стартира стотици пъти по време на живота на екрана.

Recomposition работи на принципа на smart invalidation: Compose проследява кои State обекти всяка Composable функция чете и маркира за рестартиране само тези, чиито зависимости са се променили. Това се постига чрез snapshot система, която записва всички операции по четене на State по време на изпълнение, и Composer, който свързва тези зависимости с конкретни функции.

Важно е да се разбере: рекомпозицията не означава незабавно прерисуване на екрана. Compose работи в три фази: Composition (изграждане на UI описание), Layout (изчисляване на размери и позиции) и Drawing (рисуване върху платно). Ако след рекомпозиция размерите и позицията на елементите не са се променили, фазата Layout може да бъде пропусната. Ако външният вид не се е променил — Drawing се пропуска. Тази трифазна архитектура осигурява минимални разходи за всяка актуализация на UI.

Тригери на рекомпозицията: какво предизвиква рестартиране на функции

Съществуват три основни тригера на рекомпозицията. Първи — промяна на State обект, прочетен в тялото на Composable функция. Когато mutableStateOf или derivedStateOf промени стойността си, всички функции, регистрирали четене на този State в предишната композиция, се маркират за рестартиране.

Втори тригер — промяна на параметри на Composable функция при извикването й от родителска функция. Ако родителската функция е предала нова стойност (например текстът или числото са се променили), дъщерната функция ще бъде рестартирана, дори ако не чете State вътре в себе си. Compose сравнява новите и старите стойности на параметрите чрез equals и ако са равни — функцията може да бъде пропусната.

Трети тригер — промяна на CompositionLocal чрез CompositionLocalProvider. Всички функции, които четат CompositionLocal чрез .current, се рестартират при промяна на provider. Този механизъм се използва от MaterialTheme: промяната на темата (светла/тъмна) предизвиква рекомпозиция на всички компоненти, които четат MaterialTheme.colorScheme.

kotlin
@Composable
fun RecompositionDemo() {
    var counter by remember { mutableStateOf(0) }
    var text by remember { mutableStateOf("Hello") }

    Column {
        Text("Брояч: $counter")  // рекомпозиция при промяна на брояча
        Text("Съобщение: $text")    // рекомпозиция при промяна на текста

        Button(onClick = { counter++ }) {
            Text("+1")
        }
        Button(onClick = { text = "Свят" }) {
            Text("Промяна на текста")
        }
    }
}

Натискането на бутона +1 променя counter, което предизвиква рекомпозиция само на първия Text ред и на самата Column. Вторият Text ред, който показва text, не се рестартира. Такава изолация — резултат от snapshot системата: всяка Composable функция знае само за онези State обекти, които е прочела.

Оптимизация на рекомпозицията: практически похвати

Оптимизацията на рекомпозицията започва с правилен избор на структури от данни. Използвайте неизменяеми колекции (listOf, mapOf) вместо изменяеми (mutableListOf). Compose сравнява параметрите чрез equals и ако колекцията се е променила, но equals е върнал true — функцията няма да се рестартира. За изменяеми колекции използвайте SnapshotStateList, който имплементира коректно проследяване на промени на ниво елементи.

Втори похват — изнасяне на стабилни части от UI в отделни Composable функции. Ако част от екрана не зависи от често променящо се състояние, изнесете я в отделна функция с параметри. Когато дойде рекомпозиция, стабилната функция получава същите параметри, Compose ги сравнява и пропуска изпълнението. Това е по-изгодно, отколкото да се рестартира тази част в рамките на голяма функция, където част от параметрите са се променили.

Трети похват — ключове в LazyColumn. Винаги посочвайте key за item в LazyColumn, LazyGrid и други мързеливи контейнери. Ключът позволява на Compose да идентифицира елементи при промяна на списъка: добавяне, изтриване или пренареждане. Без ключ, Compose рестартира всички елементи на списъка при всяка промяна, което при големи списъци води до забележима загуба на производителност.

kotlin
// Оптимизирана структура: стабилни части изнесени отделно
@Composable
fun OptimizedScreen(items: List<Item>) {
    Column {
        Header()                         // не зависи от елементи — без рекомпозиция
        Spacer(modifier = Modifier.height(8.dp))
        LazyColumn {
            items(items, key = { it.id }) { item ->
                ItemRow(item = item)   // рекомпозиция само за променени елементи
            }
        }
    }
}

@Composable
fun Header() {
    Text("Списък с елементи", style = MaterialTheme.typography.headlineMedium)
}

@Composable
fun ItemRow(item: Item) {
    Text(item.title)
}

Skipping и Stability в Compose

Skipping е механизъм, при който Compose пропуска изпълнението на Composable функция, ако всички нейни параметри не са се променили. За да работи skipping коректно, типовете параметри трябва да бъдат стабилни (stable). Kotlin компилаторът маркира като стабилни: примитивни типове (Int, Float, Boolean), String, ламбда функции, както и класове, чиито всички полета са стабилни и val.

Stability е анотация @Stable или @Immutable, която може да се добави към потребителски класове с данни. Ако клас съдържа изменяемо поле (var), компилаторът го смята за нестабилен и Compose не може да пропусне функции с такива параметри. За класове с var използвайте @Stable, ако гарантирате, че уведомлението за промяна ще бъде изпратено чрез snapshot системата.

Stability може да се провери чрез компилаторен флаг -P "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=. /reports". Той генерира отчет със списък на всички Composable функции и техните параметри с обозначение на stability. Ако параметър е нестабилен — skipping за тази функция е невъзможен и тя ще се рестартира при всяка рекомпозиция на родителя.

ТипСтабилностSkipping
Int, Float, BooleanСтабиленДа
StringСтабиленДа
ЛамбдаСтабиленДа
data class с val полетаСтабиленДа
data class с var полетаНестабиленНе
List<String>НестабиленНе

Обърнете внимание: List<String> се счита за нестабилен, защото това е интерфейс, а не конкретна имплементация. Използвайте immutableListOf() от библиотеката Kotlin Collections Immutable или увийте списъка в @Stable клас. Ламбдата винаги е стабилна, тъй като нейният equals сравнява само референции, а при създаване на нова ламбда на мястото на извикване, родителската функция също се рестартира.

Мониторинг на рекомпозиции в Android Studio

За мониторинг на рекомпозиции, Android Studio предоставя Layout Inspector с режим Compose Recomposition Counts. В този режим всяка Composable функция показва броя на рекомпозициите и причините за рестартиране. Това позволява бързо намиране на функции, които се рекомпозират твърде често, и определяне на основната причина — нестабилни параметри или излишни State зависимости.

Допълнителни инструменти: Compose Metrics (събиране на статистика чрез instrumentation тестове) и Recomposition Timer (измерване на времето за изпълнение на всяка функция). Google препоръчва включване на тези инструменти на етапа на профилиране и изключване в release версиите, тъй като те добавят overhead до 20% за всяка рекомпозиция.

При анализ на рекомпозиции търсете модели на unnecessary recomposition: функция се рестартира, въпреки че нейният изходен UI не би трябвало да се промени. Честа причина е използването на ламбди без remember, когато всеки път се създава нов ламбда обект и Compose счита параметъра за променен. Решение: увиване на ламбдите в remember { } с фиксирани улавяния.

kotlin
// Лошо: нова ламбда при всяка рекомпозиция на родител
@Composable
fun Parent() {
    Child(onClick = { doSomething() })  // нова ламбда всеки път
}

// Добре: remember стабилизира ламбдата
@Composable
fun Parent() {
    val onClick = remember { { doSomething() } }
    Child(onClick = onClick)  // същата референция
}

Често задавани въпроси

Означава ли рекомпозицията прерисуване на екрана?

Не, рекомпозицията е само фазата Composition. След нея се изпълняват Layout и Drawing. Ако след рекомпозиция размерите и позицията на елементите не са се променили, Layout и Drawing могат да бъдат напълно пропуснати, което спестява ресурси на GPU.

Колко често може да се случва рекомпозиция?

При анимации рекомпозицията може да се стартира до 120 пъти в секунда (120fps). За обикновено взаимодействие — 10–60 пъти в секунда. Важно е всяка рекомпозиция да се вмества в бюджета на кадъра (8–16 ms), в противен случай приложението ще забавя.

Защо функция се рекомпозира, въпреки че State не се е променил?

Причината е промяна на параметър от родителска функция. Родителят се рестартира (по своя причина) и предава нова стойност. За да избегнете това, проверете stability на параметрите и използвайте remember за стабилизиране на ламбди и изчислени стойности.

Може ли да се изключи рекомпозиция за конкретна функция?

Директно изключване няма, но има принудително пропускане чрез readInComposition — State се чете извън тялото на функцията, което не регистрира зависимост. Използвайте това внимателно: функцията няма да реагира на промени, което може да доведе до остарял UI.

Кое е по-скъпо: Composition или Recomposition?

Composition е по-скъп, защото създава всички слотове и възли на дървото от нулата. Recomposition използва повторно съществуващи слотове и само актуализира техните стойности. На практика Composition на екран отнема 2–10 ms, а рекомпозиция на един елемент — 0.1–1 ms.

Обобщение

  • Recomposition — селективно рестартиране на Composable функции при промяна на State или параметрите им
  • Snapshot system проследява зависимостите на функциите от State и планира рекомпозиция
  • Skipping е възможен само за функции със стабилни параметри (@Stable или immutable)
  • Три тригера за рекомпозиция: промяна на State, промяна на параметри, промяна на CompositionLocal
  • List<T> се счита за нестабилен — използвайте неизменяеми колекции за коректно пропускане
  • Layout Inspector показва броячи на рекомпозиции за всяка функция
  • Препоръка: изнасяйте стабилни части от UI в отделни функции и използвайте remember за ламбди

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също