SideEffect — шта је то, синхронизација стања у Jetpack Compose

Аутор: IT Sectr Објављено: 2026-06-30 Време читања: 9 мин

SideEffect — је composable функција у Jetpack Compose која извршава прослеђени блок кода при свакој успешној рекомпозицији. За разлику од LaunchedEffect и DisposableEffect, SideEffect није везан за кључеве и нема блок чишћења — он једноставно синхронизује Compose стање са спољним системима после сваког рендеровања. То га чини идеалним за ажурирање callback функција, синхронизацију са ViewPager и пренос података у Analytics SDK. Према Android Developers Documentation (2025), SideEffect се извршава стриктно након што Compose потврди успешну рекомпозицију и не извршава се ако је рекомпозиција прескочена.

Главно

  • SideEffect — side-effect API за код који се извршава након сваке успешне рекомпозиције.
  • Синхронизација — преноси Compose стање у спољне системе који не подржавају Compose.
  • Без кључева — за разлику од LaunchedEffect, SideEffect се не покреће поново, већ се извршава при свакој рекомпозицији.
  • Нема чишћења — SideEffect не пружа onDispose, намењен је само за једносмерну синхронизацију.
  • Синхроност — блок се извршава синхроно са Compose фазом композиције, без корутина.

Шта је SideEffect у Jetpack 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 би садржао застарелу референцу на тренутно стање.

kotlin
@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 и фазе композиције

Да бисте разумели SideEffect, потребно је да знате фазе извршавања Jetpack Compose. Compose пролази кроз три фазе за сваки кадар: Composition (шта приказати), Layout (где приказати), Drawing (како приказати). SideEffect се извршава на крају фазе Composition — након што су све composable функције одрадиле, али пре фазе Layout. Ово гарантује да SideEffect види коначно стање свих променљивих након рекомпозиције.

Оваква позиција у животном циклусу даје важну предност: SideEffect не може да изазове бесконачну рекомпозицију, чак и ако се стање мења унутар њега. Пошто се извршава након композиције, промене направљене унутар SideEffect биће узете у обзир тек у следећем кадру — ово спречава петље карактеристичне за промене унутар тела composable функције (када setState унутар композиције покреће нову композицију пре завршетка текуће).

Још једна карактеристика — SideEffect се не оптимизује по кључевима. Извршава се при свакој рекомпозицији без обзира на то које се стање променило. Ако је потребна прецизнија контрола (извршавање само при промени одређеног параметра), користите LaunchedEffect са кључевима или умотајте SideEffect у проверу промене преко remember.

kotlin
// SideEffect оптимизован са remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }

SideEffect {
    if (currentZoom != zoomLevel) {
        map.animateToZoom(zoomLevel)
        currentZoom = zoomLevel
    }
}

// Без ове провере SideEffect би позвао animateToZoom
// при свакој рекомпозицији, чак и ако се zoomLevel није променио

Ажурирање callback функција преко SideEffect

Најчешћи практични сценариј за SideEffect — ажурирање callback функција које затварају тренутно стање. У Jetpack Compose ово се назива „управљање животним циклусом callback-а“. Проблем је у томе што lambda изрази у Kotlin-у хватају променљиве по референци, и ако је callback креиран са једном вредношћу променљиве, а потом се променљива променила — callback наставља да користи стару вредност.

Размотримо пример: Google Maps SDK за Android прихвата објекат OnCameraMoveListener преко setOnCameraMoveListener(). Ако проследите lambdu која хвата isTrackingEnabled, онда када се isTrackingEnabled промени, lambda се неће ажурирати — Maps SDK ће наставити да позива стари callback са застарелим подацима. SideEffect решава овај проблем: поново поставља listener при свакој рекомпозицији, гарантујући да SDK увек користи ажурну lambdu са тренутним стањем.

Према Документацији Maps SDK за Android (2025), Google препоручује управо овај образац при интеграцији Maps са Jetpack Compose. Сличан приступ се користи за WebView, VideoView, TextureView и било које друге View-based компоненте које прихватају callback-ове преко set-метода. SideEffect гарантује ажурност callback-ова при свакој промени стања.

kotlin
@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 })
}

Синхронизација са Analytics SDK

Још један важан сценариј SideEffect — слање догађаја у аналитичке системе при промени UI стања. На пример, када корисник пребацује картице у TabLayout унутар Compose екрана, SideEffect може да преноси тренутно стање изабране картице у Firebase Analytics или AppsFlyer. Сваки пут када се изабрана картица промени (и дође до рекомпозиције), SideEffect шаље одговарајући догађај.

Разлика у односу на слање догађаја директно у onClick или onTabSelected је у томе што SideEffect реагује на промену стања изазвану било којим начином — не само радњом корисника, већ и програмском променом, враћањем стања након ротације екрана или Deep Link-ом. Ово чини SideEffect универзалним механизмом за синхронизацију, независним од извора промене.

Према Firebase Best Practices (Google, 2025), слање аналитичких догађаја преко SideEffect даје потпунију слику корисничког пута, јер бележи све промене стања, укључујући и оне које се дешавају без директне радње корисника. Међутим, важно је не претерати: сваки догађај у Analytics-у је мрежни захтев, па за стања која се често мењају (позиција скроловања, координате прста) SideEffect није погодан — користите debounce или шаљите догађаје само при значајним променама.

kotlin
@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 vs LaunchedEffect: када шта користити

Избор између SideEffect и LaunchedEffect зависи од два фактора: да ли је потребна асинхроност и да ли је потребно управљање по кључевима. SideEffect је синхрон и извршава се при свакој рекомпозицији. LaunchedEffect је асинхрон (корутина) и извршава се само при промени кључа, не при свакој рекомпозицији.

Ако треба да извршите акцију при свакој промени UI — користите SideEffect. Ако треба да извршите акцију једном при појављивању екрана или при промени одређеног параметра — користите LaunchedEffect са кључевима. Ако је потребна асинхрона операција (учитавање података, кашњење, рад са Flow) — само LaunchedEffect, јер SideEffect не подржава suspend функције.

КарактеристикаSideEffectLaunchedEffect
ИзвршавањеПри свакој рекомпозицијиПри промени кључева
АсинхроностСинхронКорутина
КључевиНемаИма (vararg)
ЧишћењеНемаАутоматско отказивање корутине
Типична применаCallback-ови, Analytics, View синхронизацијаУчитавање, Flow претплата, тајмери

У пракси 70% случајева коришћења side effects покрива LaunchedEffect (асинхроне операције, учитавање података), 20% — DisposableEffect (ресурси са чишћењем) и само 10% — SideEffect (синхронизација callback функција). SideEffect је специјализовани алат за уски круг задатака, али у тим задацима је незаменљив.

Типичне грешке са SideEffect

Главна грешка — мењање Compose стања унутар SideEffect. Иако SideEffect не изазива бесконачну петљу директно (пошто се извршава након фазе композиције), може да изазове прекомерне рекомпозиције. Ако се унутар SideEffect промени стање (mutableStateOf), ово покреће нову рекомпозицију у следећем кадру, која поново извршава SideEffect — и тако до стабилизације. Ово није бесконачна петља, али је додатни посао за framework.

Друга грешка — извршавање тешких прорачуна унутар SideEffect. Пошто се SideEffect позива при свакој рекомпозицији, а рекомпозиције се могу дешавати десетине пута у секунди (при анимацијама, скроловању), било који тежак код унутар SideEffect довешће до пада кадрова. Изместите тешке операције ван композиције — у корутину (LaunchedEffect) или израчунавајте преко derivedStateOf / remember.

Трећа грешка — покушај коришћења SideEffect за асинхрони код. SideEffect није suspend функција, па се delay(), await(), collect() и друге корутинске операције унутар њега неће компајлирати. Ако је потребно извршити асинхрону акцију након рекомпозиције, користите snapshotFlow { ... } у комбинацији са LaunchedEffect или покрените корутину преко rememberCoroutineScope.

Често постављана питања

Да ли се SideEffect извршава при првој композицији?

Да, SideEffect се извршава при свакој успешној композицији, укључујући и прву — када се компонент тек појављује на екрану. Ово га разликује од LaunchedEffect(Unit) који се такође извршава једном при првој композицији, али се не извршава при наредним рекомпозицијама (ако се кључ није променио).

Може ли SideEffect изазвати бесконачну петљу?

Не, SideEffect се извршава након фазе композиције — промене направљене унутар њега биће примењене тек у следећем кадру, што спречава петље. Међутим, честа промена стања унутар SideEffect може изазвати лавину рекомпозиција, смањујући перформансе. Мењајте стање унутар SideEffect само када је стварно потребно.

Која је разлика између SideEffect и snapshotFlow?

SideEffect се извршава синхроно при свакој рекомпозицији. snapshotFlow креира Flow из Compose стања и може се користити са collectLatest у LaunchedEffect за реактивну обраду промена. snapshotFlow је погодан за случајеве када је потребно реаговати на промене са debounce, filter или distinctUntilChanged — што није могуће у синхроном SideEffect.

Како отклонити грешке ако се SideEffect извршава пречесто?

Користите Android Studio Compose Modifier Debugger или додајте логирање са именом компонента и учесталошћу позива. Ако се SideEffect извршава чешће него што се очекује, проверите да ли се стање родитељског компонента мења без потребе. Оптимизација: издвојите стабилне делове UI у засебне composable функције са unstable-анoтацијама да бисте смањили број рекомпозиција.

Може ли се комбиновати SideEffect са DisposableEffect?

Да, могу се користити у истом компоненту за различите сврхе. DisposableEffect је задужен за подешавање и чишћење ресурса (једном), а SideEffect — за синхронизацију тренутног стања са овим ресурсом при свакој рекомпозицији. Типичан пример: DisposableEffect региструје callback преко API-ја, а SideEffect ажурира затворене податке у овом callback-у при свакој промени.

Закључак

  • SideEffect — side-effect API за синхрони код који се извршава након сваке успешне рекомпозиције у Jetpack Compose.
  • Синхронизација — основни сценариј: пренос Compose стања у спољне системе (Google Maps, WebView, ViewPager, Analytics SDK).
  • Callback-ови — SideEffect гарантује ажурност callback функција које затварају тренутно стање при сваком ажурирању UI.
  • Без петљи — извршава се након фазе композиције, па промена стања унутар SideEffect не изазива бесконачну рекомпозицију.
  • Ограничења — не подржава кључеве, асинхроност и блок чишћења; за ове задатке користите LaunchedEffect или DisposableEffect.
  • Перформансе — избегавајте тешке прорачуне унутар SideEffect, јер се извршава при свакој рекомпозицији (до 60 пута у секунди).
  • Отклањање грешака — контролишите учесталост позива преко Compose Debugger и оптимизујте преко remember за филтрирање непотребних рекомпозиција.

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође