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 оптимизиран с remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }
SideEffect {
if (currentZoom != zoomLevel) {
map.animateToZoom(zoomLevel)
currentZoom = zoomLevel
}
}
// Без тази проверка SideEffect би извикал animateToZoom
// при всяка рекомпозиция, дори ако zoomLevel не се е променил
Най-честият практически сценарий за SideEffect — актуализиране на callback функции, които затварят текущото състояние. В Jetpack Compose това се нарича „управление на жизнения цикъл на callback“. Проблемът е, че ламбда изразите в Kotlin улавят променливи по референция и ако callback е създаден с една стойност на променлива, а след това променливата се е променила — callback продължава да използва старата стойност.
Нека разгледаме пример: Google Maps SDK за Android приема обект OnCameraMoveListener чрез setOnCameraMoveListener(). Ако подадете ламбда, която улавя isTrackingEnabled, то при промяна на isTrackingEnabled, ламбдата няма да се актуализира — Maps SDK ще продължи да извиква стария callback с остарели данни. SideEffect решава този проблем: той пренастройва listener при всяка рекомпозиция, гарантирайки че SDK винаги използва актуална ламбда с текущото състояние.
Според Документацията на Maps SDK за Android (2025), Google препоръчва точно този модел при интегриране на Maps с Jetpack Compose. Подобен подход се използва за WebView, VideoView, TextureView и всички други View-базирани компоненти, които приемат callback-ове чрез set-методи. SideEffect гарантира актуалността на callback-овете при всяка промяна на състояние.
@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 с TabRow и избран раздел
}
Изборът между SideEffect и LaunchedEffect зависи от два фактора: необходима ли е асинхронност и необходимо ли е управление чрез ключове. SideEffect е синхронен и се изпълнява при всяка рекомпозиция. LaunchedEffect е асинхронен (корутина) и се изпълнява само при промяна на ключа, не при всяка рекомпозиция.
Ако трябва да изпълните действие при всяка промяна на UI — използвайте SideEffect. Ако трябва да изпълните действие веднъж при появяване на екрана или при промяна на конкретен параметър — използвайте LaunchedEffect с ключове. Ако е необходима асинхронна операция (зареждане на данни, закъснение, работа с Flow) — само LaunchedEffect, тъй като SideEffect не поддържа suspend функции.
| Характеристика | SideEffect | LaunchedEffect |
|---|---|---|
| Изпълнение | При всяка рекомпозиция | При промяна на ключове |
| Асинхронност | Синхронен | Корутина |
| Ключове | Не | Да (vararg) |
| Почистване | Не | Автоматично отменяне на корутина |
| Типично приложение | Callback-ове, Analytics, View синхронизация | Зареждане, Flow абонамент, таймери |
На практика 70% от случаите на използване на side effects се покриват от LaunchedEffect (асинхронни операции, зареждане на данни), 20% от DisposableEffect (ресурси с почистване) и само 10% от SideEffect (синхронизация на callback функции). SideEffect е специализиран инструмент за тесен кръг от задачи, но в тези задачи е незаменим.
Основната грешка — промяна на Compose състояние вътре в SideEffect. Въпреки че SideEffect не причинява директно безкраен цикъл (тъй като се изпълнява след фазата на композиция), може да причини прекомерни рекомпозиции. Ако състоянието (mutableStateOf) се промени вътре в SideEffect, това задейства нова рекомпозиция в следващия кадър, която отново изпълнява 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също