SideEffect — це composable-функція в Jetpack Compose, яка виконує переданий блок коду при кожній успішній рекомпозиції. На відміну від LaunchedEffect та DisposableEffect, SideEffect не прив'язаний до ключів і не має блоку очищення — він просто синхронізує стан Compose із зовнішніми системами після кожного рендерингу. Це робить його ідеальним для оновлення callback-функцій, синхронізації з ViewPager та передачі даних в Analytics SDK. За даними Android Developers Documentation (2025), SideEffect виконується строго після того, як Compose підтвердив успішну рекомпозицію, і не виконується, якщо рекомпозицію було пропущено.
Головне
SideEffect — це найпростіший із side-effect API у Jetpack Compose. Він виконує блок коду при кожній успішній рекомпозиції composable-компонента. Слово «успішній» тут ключове: якщо Compose вирішив, що рекомпозиція не потрібна (наприклад, всі вхідні параметри не змінилися і результат буде тим самим), SideEffect не виконується. Це гарантує, що блок синхронізації викликається тільки тоді, коли UI дійсно змінився.
Основний сценарій використання SideEffect — синхронізація стану Compose з об'єктами, які не є частиною дерева Compose. Типові приклади: оновлення callback-функції в Legacy View-системі, передача поточного стану в ViewPager, надсилання події в Analytics SDK при зміні даних, що відображаються, синхронізація з картографічними SDK, які очікують оновлень у зовнішньому форматі.
За даними Android Developer Blog (2025), SideEffect часто використовується в зв'язці з remember: remember зберігає об'єкт (наприклад, callback), а SideEffect оновлює його при кожній зміні залежності. Цей патерн особливо важливий для бібліотек, які приймають listener-об'єкти і не перестворюють їх при оновленні — без SideEffect listener би містив застаріле посилання на поточний стан.
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setZoom(zoomLevel)
mapView.updateMarkers(markers)
}
AndroidView(factory = { mapView })
}
Щоб зрозуміти SideEffect, потрібно розібратися з фазами виконання Jetpack Compose. Compose проходить три фази для кожного кадру: Composition (що відобразити), Layout (де відобразити), Drawing (як відобразити). SideEffect виконується в кінці фази Composition — після того, як всі composable-функції відпрацювали, але до фази Layout. Це гарантує, що SideEffect бачить фінальний стан всіх змінних після рекомпозиції.
Таке розташування в життєвому циклі дає важливу перевагу: SideEffect не може викликати нескінченну рекомпозицію, навіть якщо всередині нього змінюється стан. Оскільки він виконується після композиції, зміни, зроблені всередині SideEffect, будуть враховані тільки в наступному кадрі — це запобігає циклам, характерним для змін всередині тіла composable-функції (коли setState всередині композиції тригерить нову композицію до завершення поточної).
Ще одна особливість — SideEffect не оптимізується за ключами. Він виконується при кожній рекомпозиції незалежно від того, який саме стан змінився. Якщо потрібен більш точний контроль (виконувати тільки при зміні конкретного параметра), використовуйте LaunchedEffect з ключами або обгортайте SideEffect у перевірку зміни через remember.
// SideEffect optimized with remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }
SideEffect {
if (currentZoom != zoomLevel) {
map.animateToZoom(zoomLevel)
currentZoom = zoomLevel
}
}
// Without this check SideEffect would call animateToZoom
// on every recomposition, even if zoomLevel didn't change
Найчастіший практичний сценарій для SideEffect — оновлення callback-функцій, що замикають поточний стан. У Jetpack Compose це називається «callback lifecycle management». Проблема в тому, що lambda-вирази в Kotlin захоплюють змінні за посиланням, і якщо callback був створений з одним значенням змінної, а потім змінна змінилася — callback продовжує використовувати старе значення.
Розглянемо приклад: Google Maps SDK для Android приймає об'єкт OnCameraMoveListener через setOnCameraMoveListener(). Якщо передати лямбду, яка захоплює isTrackingEnabled, то при зміні isTrackingEnabled лямбда не оновиться — Maps SDK продовжить викликати старий колбек із застарілими даними. SideEffect вирішує цю проблему: він перевстановлює listener при кожній рекомпозиції, гарантуючи, що SDK завжди використовує актуальну лямбду з поточним станом.
За даними Документації Maps SDK для Android (2025), Google рекомендує саме такий патерн при інтеграції Maps з Jetpack Compose. Аналогічний підхід використовується для WebView, VideoView, TextureView та будь-яких інших View-компонентів, які приймають колбеки через set-методи. SideEffect гарантує актуальність колбеків при кожній зміні стану.
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setOnMarkerClickListener { marker ->
onMarkerClick(marker)
true
}
mapView.isTrafficEnabled = isTrackingEnabled
}
AndroidView(factory = { mapView })
}
Ще один важливий сценарій SideEffect — надсилання подій в аналітичні системи при зміні стану UI. Наприклад, коли користувач перемикає вкладки в TabLayout всередині Compose-екрана, SideEffect може передавати поточний стан вибраної вкладки в Firebase Analytics або AppsFlyer. Кожного разу, коли вибрана вкладка змінюється (і відбувається рекомпозиція), SideEffect надсилає відповідну подію.
Відмінність від надсилання подій безпосередньо в onClick або onTabSelected в тому, що SideEffect спрацьовує на зміну стану, викликану будь-яким способом — не тільки дією користувача, але й програмною зміною, відновленням стану після повороту екрану або Deep Link. Це робить SideEffect універсальним механізмом для синхронізації, незалежним від джерела зміни.
За даними Firebase Best Practices (Google, 2025), надсилання аналітичних подій через SideEffect дає більш повну картину шляху користувача, оскільки фіксує всі зміни стану, включаючи ті, що відбуваються без прямої дії користувача. Однак важливо не перестаратися: кожна подія в Analytics — це мережевий запит, тому для часто змінюваних станів (позиція скролу, координати пальця) SideEffect не підходить — використовуйте debounce або надсилайте події тільки при значних змінах.
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
SideEffect {
val params = Bundle().apply {
putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
putString(FirebaseAnalytics.Param.ITEM_ID, productId)
}
firebaseAnalytics.logEvent( FirebaseAnalytics.Event.VIEW_ITEM, params)
}
// UI with TabRow and selected tab
}
Вибір між SideEffect та LaunchedEffect залежить від двох факторів: чи потрібна асинхронність і чи потрібне управління за ключами. SideEffect — синхронний і виконується при кожній рекомпозиції. LaunchedEffect — асинхронний (корутина) і виконується тільки при зміні ключа, а не при кожній рекомпозиції.
| Характеристика | SideEffect | LaunchedEffect |
|---|---|---|
| Виконання | При кожній рекомпозиції | При зміні ключів |
| Асинхронність | Синхронний | Корутина |
| Ключі | Ні | Так (vararg) |
| Очищення | Ні | Автоматичне скасування корутини |
| Типове застосування | Колбеки, Analytics, View-синхронізація | Завантаження, Flow підписка, таймери |
На практиці 70% випадків використання side effects покриваються LaunchedEffect (асинхронні операції, завантаження даних), 20% — DisposableEffect (ресурси з очищенням) і тільки 10% — SideEffect (синхронізація callback-функцій). SideEffect — це спеціалізований інструмент для вузького кола задач, але в цих задачах він незамінний.
Головна помилка — зміна стану Compose всередині SideEffect. Хоча SideEffect не викликає нескінченний цикл безпосередньо (оскільки виконується після фази композиції), він може викликати надлишкові рекомпозиції. Якщо всередині SideEffect змінити стан (mutableStateOf), це тригерить нову рекомпозицію в наступному кадрі, яка знову виконає SideEffect — і так до стабілізації. Це не нескінченний цикл, але зайва робота для фреймворку.
Друга помилка — виконання важких обчислень всередині SideEffect. Оскільки SideEffect викликається при кожній рекомпозиції, а рекомпозиції можуть відбуватися десятки разів на секунду (при анімаціях, скролі), будь-який важкий код всередині SideEffect призведе до падіння кадрів. Виносьте важкі операції за межі композиції — в корутину (LaunchedEffect) або обчислюйте через derivedStateOf / remember.
Третя помилка — спроба використовувати SideEffect для асинхронного коду. SideEffect не є suspend-функцією, тому delay(), await(), collect() та інші корутинні операції всередині нього не скомпілюються. Якщо потрібно виконати асинхронну дію після рекомпозиції, використовуйте snapshotFlow { ... } у комбінації з LaunchedEffect або запускайте корутину через rememberCoroutineScope.
Часто задавані питання
Так, SideEffect виконується при кожній успішній композиції, включаючи найпершу — коли компонент тільки з'являється на екрані. Це відрізняє його від LaunchedEffect(Unit), який теж виконується один раз при першій композиції, але не виконується при наступних рекомпозиціях (якщо ключ не змінився).
Ні, SideEffect виконується після фази композиції — зміни, зроблені всередині нього, будуть застосовані тільки в наступному кадрі, що запобігає циклам. Однак часта зміна стану всередині SideEffect може викликати лавину рекомпозицій, знижуючи продуктивність. Змінюйте стан всередині SideEffect тільки при реальній необхідності.
SideEffect виконується синхронно при кожній рекомпозиції. snapshotFlow створює Flow зі стану Compose і може використовуватися з collectLatest в LaunchedEffect для реактивної обробки змін. snapshotFlow підходить для випадків, коли потрібно реагувати на зміни з debounce, filter або distinctUntilChanged — що неможливо в синхронному SideEffect.
Використовуйте Android Studio Compose Modifier Debugger або додайте логування із зазначенням імені компонента та частоти викликів. Якщо SideEffect виконується частіше, ніж очікувалося, перевірте, чи не змінюється стан батьківського компонента без необхідності. Оптимізація: винесіть стабільні частини UI в окремі composable-функції з unstable-анотаціями для зменшення кількості рекомпозицій.
Так, їх можна використовувати в одному компоненті для різних цілей. DisposableEffect відповідає за налаштування та очищення ресурсу (один раз), а SideEffect — за синхронізацію поточного стану з цим ресурсом при кожній рекомпозиції. Типовий приклад: DisposableEffect реєструє callback через API, а SideEffect оновлює замикані дані в цьому callback при кожній зміні.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також