Composition — це центральний процес у Jetpack Compose, під час якого з описових Composable-функцій будується живе UI-дерево, що відображається на екрані. На відміну від View-системи Android, де розмітка завантажувалася з XML і перетворювалася на незмінні об'єкти, Composition працює як динамічна система: функції виконуються, створюють слоти в пам'яті, формують ієрархію вузлів і пов'язують її зі станом. За даними Google Android Developers, 2026, розуміння Composition критично важливе для оптимізації продуктивності Compose-застосунків.
Головне
Composition — це процес виконання Composable-функцій, у результаті якого формується внутрішнє представлення користувацького інтерфейсу у вигляді дерева вузлів. Кожен вузол цього дерева відповідає або вбудованому компоненту (Text, Button, Image), або виклику користувацької Composable-функції. Composition не створює безпосередньо View-об'єкти Android — він будує абстрактний опис, який потім обробляється фазами Layout і Drawing.
Ключова особливість Composition — його енергонезалежність (restartability). Кожна Composable-функція в складі композиції може бути перезапущена в будь-який момент, якщо змінилися її вхідні параметри або прочитані нею state-об'єкти. Система не перезапускає все дерево цілком — лише ті функції, які дійсно залежать від змінених даних.
Технічно Composition керується через Composer — внутрішній двигун, який Kotlin-компілятор вбудовує в кожну Composable-функцію. Composer записує в слоти (групи позицій) інформацію про те, які функції були викликані, з якими параметрами та в якому порядку. При подальших викликах Composer порівнює нові дані зі збереженими та приймає рішення про перезапуск.
Процес побудови UI-дерева починається з виклику методу setContent всередині Activity або Fragment. Цей метод створює початковий Composition і запускає виконання кореневої Composable-функції. Далі кожна вкладена Composable-функція додає свої вузли до дерева, формуючи ієрархію: Row містить Text і Button, Column містить Image і Card, і так далі.
Кожен вузол дерева отримує унікальний ключ позиції, заснований на його положенні у вихідному коді. Цей ключ використовується для ідентифікації вузла при повторних виконаннях. Ключ позиції — причина, через яку порядок виклику Composable-функцій не повинен залежати від умов: якщо в одному запуску викликано A -> B, а в наступному B -> A, Compose не зможе зіставити старі та нові вузли.
@Composable
fun AppScreen() {
Column { // Вузол Column (позиція 1)
HeaderSection() // Вузол HeaderSection (позиція 2)
ContentSection() // Вузол ContentSection (позиція 3)
FooterSection() // Вузол FooterSection (позиція 4)
}
}
@Composable
fun HeaderSection() {
Row { // Вузол Row (позиція 2.1)
Text("Заголовок") // Вузол Text (позиція 2.2)
Icon(...) // Вузол Icon (позиція 2.3)
}
}
У цьому прикладі кожен виклик отримує позицію, засновану на порядку в коді. Column (позиція 1) містить три дочірніх вузли (позиції 2, 3, 4). HeaderSection додає ще два дочірніх вузли (2.1, 2.2, 2.3). Якщо в наступній рекомпозиції ContentSection буде викликано до HeaderSection, Composer не зможе коректно зіставити вузли — звідси правило: порядок викликів Composable-функцій повинен бути стабільним.
Стан у Composition керується через об'єкти типу State<T>. Коли Composable-функція читає значення з State через делеговану властивість (by), вона реєструє залежність від цього State. При зміні значення всі функції, які прочитали цей State, позначаються для перезапуску в наступній фазі композиції.
Механізм реєстрації залежностей називається snapshot system. Кожного разу, коли State змінюється, знімок (snapshot) фіксує всі зміни та повідомляє Composer, які функції залежать від цього State. Важливо розуміти: читання State всередині не-Composable коду (наприклад, у лямбді onClick) не реєструє залежність — лише читання всередині Composable-функції або в лямбдах, виконаних у контексті композиції.
Snapshot system працює транзакційно: кілька змін State в рамках однієї події об'єднуються в одну транзакцію, що запобігає множинним рекомпозиціям. Це особливо важливо при обробці жестів: за один рух змінюється кілька State-об'єктів, але Compose виконує лише одну рекомпозицію.
@Composable
fun StateExample() {
var text by remember { mutableStateOf("Hello") }
var isVisible by remember { mutableStateOf(true) }
Column {
Text(text) // реєструє залежність від text
if (isVisible) { // реєструє залежність від isVisible
TextField(value = text, onValueChange = { text = it })
}
Button(onClick = { isVisible = !isVisible }) {
Text(if (isVisible) "Приховати" else "Показати")
}
}
}
Зміна text призводить до рекомпозиції лише Column, Text і TextField. Column, Button і умова isVisible залишаються без змін. Така ізольованість рекомпозиції — ключова перевага Compose перед системами, які перемальовують весь екран цілком. Кожна Composable-функція відстежує лише ті State-об'єкти, які вона безпосередньо читає.
Composition (композиція) і Recomposition (рекомпозиція) — два різних режими виконання Composable-функцій. Composition відбувається один раз при створенні екрана: система виконує всі Composable-функції з початковими значеннями та будує початкове UI-дерево. Recomposition відбувається багаторазово при зміні даних: система перезапускає лише ті функції, які залежать від зміненого стану.
Режим Composition активує всі вузли дерева, виділяє слоти для кожної функції, реєструє всіх нащадків. Recomposition працює вибірково: Compose порівнює нові та старі значення параметрів кожної функції, і якщо вони не змінилися — функція не виконується (skipping).
Composition і Recomposition різняться вартістю. Перший Composition дорожчий, оскільки потребує повної побудови дерева та виділення слотів. Recomposition дешевший, особливо якщо більшість функцій stable — їх параметри порівнюються за equals, і Compose пропускає їх виклик. Для максимальної продуктивності потрібно прагнути до того, щоб більшість рекомпозицій зачіпало якомога менше функцій.
| Характеристика | Composition | Recomposition |
|---|---|---|
| Коли відбувається | Один раз, при першому відображенні | Багаторазово, при зміні даних |
| Обсяг | Все дерево цілком | Лише змінені функції |
| Порівняння параметрів | Не виконується | Виконується для skipping |
| Створення слотів | Так, всі слоти створюються | Лише для нових вузлів |
CompositionLocal — механізм неявної передачі даних через дерево композиції. Він вирішує проблему, коли параметр потрібно передати через десятки вкладених Composable-функцій, які його не використовують безпосередньо. Замість явного ланцюжка параметрів дані встановлюються на верхньому рівні та читаються в будь-якій вкладеній функції через CompositionLocal.current.
Матеріальна тема MaterialTheme — найвідоміший приклад CompositionLocal. Всі компоненти Compose читають кольори, типографіку та форми через MaterialTheme.colorScheme, MaterialTheme.typography, MaterialTheme.shapes, не отримуючи їх через параметри. Розробник може створювати свої CompositionLocal для таких даних, як поточний користувач, налаштування локалізації або конфігурація екрана.
Важливе обмеження: CompositionLocal не слід використовувати для даних, що часто змінюються (положення скролу, текст у полі введення). Компонент, який читає CompositionLocal, перезапускається при кожній зміні значення, тому для динамічних даних краще використовувати явні параметри або State. CompositionLocal оптимальний для конфігураційних даних, які змінюються рідко або не змінюються взагалі.
val LocalUser = compositionLocalOf<User?> { null }
@Composable
fun AppRoot(user: User, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalUser.provides(user)) {
content()
}
}
@Composable
fun UserAvatar() {
val user = LocalUser.current // читання без явного параметра
AsyncImage(model = user?.avatarUrl, contentDescription = "Avatar")
}
CompositionLocalProvider створює область видимості, всередині якої LocalUser.current повертає задане значення. UserAvatar читає користувача без явної передачі параметра через проміжні функції. Це особливо цінно в глибоких ієрархіях, де дані потрібні лише в кількох листових вузлах.
Часто задавані питання
Зміна State під час Composition планує нову рекомпозицію, яка виконається після завершення поточної. Зациклення не відбувається: Compose гарантує, що кожна рекомпозиція виконується в окремій транзакції snapshot-системи.
На сучасних пристроях Composition екрана з 50–100 Composable-функціями займає 1–5 мс. Google рекомендує вкладатися в 16 мс для кадру 60fps. Якщо Composition перевищує цей ліміт, використовуйте LazyColumn або розбийте екран на менші функції.
Прямий ручний запуск Composition неможливий — він керується Composer автоматично. Однак можна примусово запланувати рекомпозицію, змінивши State або викликавши invalidate() у кореневого композита, якщо є доступ до CompositionContext.
View-ієрархія — це незмінне дерево Java-об'єктів, яке створюється один раз. Composition — віртуальне дерево, яке перебудовується при кожній зміні даних. View зберігає свій стан у змінних екземпляра, Composition — у слотах, прив'язаних до позиції виклику функції.
Якщо Composable-функція перестала викликатися (наприклад, умова if стала false), Composition видаляє її вузол і викликає очищення DisposableEffect. При повторній появі (if знову true) створюється новий вузол — старий не відновлюється.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також