DisposableEffect је composable-функција у Jetpack Compose-у намењена операцијама које захтевају експлицитну иницијализацију и накнадно чишћење ресурса. За разлику од других side-effect API-ја, DisposableEffect пружа блок onDispose који се гарантовано извршава при изласку компоненте из композиције или при промени кључа. Ово га чини незаменљивим за рад са домаћим претплатама, слушаоцима сензора и хардверским ресурсима. Према Android Developers Documentation (2025), DisposableEffect се препоручује у свим сценаријима где је потребан пар setup/teardown, аналоган onStart/onStop у животном циклусу Activity-ја.
Главно
DisposableEffect је кључни алат за управљање ресурсима у Jetpack Compose-у. Његова главна карактеристика је гарантовани позив блока onDispose при завршетку животног циклуса composable-компоненте. Ово понашање је критично за Android развој, где незатворене претплате на системске сервисе могу довести до цурења меморије и пада апликације.
За разлику од LaunchedEffect-а, који ради у асинхроном контексту корутине, DisposableEffect се извршава синхроно. То значи да се унутар њега не могу позивати suspend-функције. Синхроност обезбеђује предвидљивост: можете бити сигурни да ће се код иницијализације извршити пре првог приказивања, а код чишћења — пре него што компонента буде уклоњена из меморије.
Према Документацији Jetpack Compose-а (2025), DisposableEffect треба користити у четири основна сценарија: (1) претплата на системске сервисе (сензори, LocationManager), (2) регистрација BroadcastReceiver-а, (3) рад са callback-базираним библиотекама које не подржавају корутине, (4) повезивање Compose-компоненти са Legacy View системима путем AndroidView-а.
class SensorManager(private val context: Context) {
fun startListening(callback: (Float) -> Unit) { /* register */ }
fun stopListening() { /* cancel */ }
}
@Composable
fun SensorDisplay() {
val sensorManager = remember { SensorManager(context) }
var value by remember { mutableStateOf(0f) }
DisposableEffect(Unit) {
sensorManager.startListening { value = it }
onDispose { sensorManager.stopListening() }
}
Text("Сензор: $value")
}
Унутрашња механика DisposableEffect-а се заснива на фазама животног циклуса композиције. Када composable-компонента уђе у композицију, DisposableEffect позива прослеђени блок кода. Овај блок враћа објекат DisposableEffectResult који садржи ламбда onDispose. Композиција чува овај резултат и позива onDispose у тренутку када компонента напушта композицију — без обзира на разлог (навигација, промена стања родитеља, уклањање из LazyColumn-а).
Механизам кључева у DisposableEffect-у ради аналогно LaunchedEffect-у: при промени било ког кључа прво се извршава onDispose за старо стање, затим се блок иницијализације покреће поново са новим кључевима. Ово омогућава реконфигурисање ресурса при промени његових параметара. На пример, ако је кључ URL сокета, при његовој промени стари сокет се затвара и отвара се нови.
Важно: блок onDispose је обавезни елемент DisposableEffect-а. Ако се onDispose не позове унутар блока, код се неће компајлирати. Овај захтев компајлера гарантује да програмер неће заборавити да предвиди чишћење ресурса, што је чест узрок грешака при ручном управљању претплатама.
// Исправна употреба са кључем
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
// Више ресурса у једном DisposableEffect-у
DisposableEffect(Unit) {
context.registerReceiver(receiver, intentFilter)
lifecycle.addObserver(observer)
onDispose {
context.unregisterReceiver(receiver)
lifecycle.removeObserver(observer)
}
}
Цурење меморије у Android апликацијама често настаје због нерегистрованих слушалаца и претплата које настављају да држе референцу на Activity или Context након што је екран затворен. DisposableEffect решава овај проблем на нивоу framework-а: ако је програмер користио DisposableEffect за регистрацију слушаоца, onDispose ће гарантовано отказати претплату у било ком сценарију завршетка компоненте.
Посебно је критично за LazyColumn и LazyGrid, где се елементи стално креирају и уништавају током скроловања. Без DisposableEffect-а, сваки елемент који нестаје из видног поља остављао би активну претплату. Са DisposableEffect-ом, onDispose се позива за сваки истоварени елемент, гарантујући да се ресурси ослобађају одмах након што елемент напусти екран.
Према Android Performance Patterns (Google, 2025), коришћење DisposableEffect-а за све домаће претплате смањује број цурења меморије у Compose апликацијама за 60–70% у поређењу са ручним управљањем путем lifecycle-повратних позива. Систем сам прати тренутак изласка из композиције и гарантује извршење onDispose-а чак и при хитном затварању екрана.
| Ресурс | Шта ради DisposableEffect | Без DisposableEffect-а |
|---|---|---|
| BroadcastReceiver | register + onDispose → unregister | Receiver остаје активан |
| SensorManager | registerListener + onDispose → unregister | Сензор наставља да шаље податке |
| Observable (не Flow) | subscribe + onDispose → unsubscribe | Callback држи референцу |
| TextureView / SurfaceView | setCallback + onDispose → removeCallback | Цурење callback-а |
| Socket / Channel | open + onDispose → close | Веза остаје отворена |
Један од најилустративнијих примера коришћења DisposableEffect-а — рад са сензорима уређаја (акцелерометар, жироскоп, магнетометар). Сензори захтевају обавезно отказивање регистрације при завршетку рада, иначе настављају да троше енергију батерије и шаљу податке чак и након затварања екрана.
Практични пример: апликација за мерење угла нагиба. DisposableEffect(Unit) региструје слушаоца акцелерометра при појављивању компоненте и отказује регистрацију у onDispose-у. Подаци сензора се преносе у стање путем mutableStateOf-а, што аутоматски ажурира UI. Ако се екран скролује у LazyColumn-у и елемент нестане, onDispose се активира тренутно — сензор престаје да шаље податке за овај елемент.
При промени типа сензора (на пример, са акцелерометра на жироскоп) кључ sensorType се мења, onDispose отказује стару претплату, а нови DisposableEffect блок региструје нови сензор. Без кључева, морало би се ручно проверавати који сензор је претходно регистрован и позвати unregisterListener са одговарајућим listener-ом — што је склоно грешкама.
@Composable
fun SensorReadingScreen(sensorType: Int) {
val context = LocalContext.current
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
var sensorValue by remember { mutableStateOf(0f) }
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
val listener = SensorEventListener { event, _ ->
sensorValue = event.values[0]
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
Text("Вредност: $sensorValue")
}
BroadcastReceiver је класичан пример API-ја који захтева обавезни пар register / unregister. У Compose апликацији, DisposableEffect је идеалан за регистрацију пријемника за време живота одређеног екрана. При уласку на екран, BroadcastReceiver се региструје са одговарајућим IntentFilter-ом, при изласку — аутоматски се отказује у onDispose-у.
Типични сценарио — праћење стања мреже. DisposableEffect региструје пријемник на ConnectivityManager који обавештава о променама мрежне конекције. При промени статуса (WiFi / мобилни подаци / без мреже) ажурира се стање composable-а, а UI приказује одговарајући индикатор. Када се екран затвори, onDispose гарантовано отказује регистрацију — чак и ако апликација пређе у позадину.
За пријемнике са ContextCompat.registerReceiver и заставом RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED (Android 14+) употреба DisposableEffect-а постаје обавезна, јер систем захтева експлицитно навођење домена деловања пријемника. DisposableEffect гарантује да је домен деловања ограничен на време живота екрана, што се поклапа са безбедносним захтевима нових верзија Android-а.
@Composable
fun NetworkStatusBanner() {
val context = LocalContext.current
var isConnected by remember { mutableStateOf(true) }
DisposableEffect(Unit) {
val receiver = BroadcastReceiver { _, _ ->
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
isConnected = cm.getActiveNetwork() != null
}
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).let { filter ->
context.registerReceiver(receiver, filter)
}
onDispose {
context.unregisterReceiver(receiver)
}
}
if (!isConnected) { ... }
}
Прва критична грешка — изостанак позива onDispose. Код унутар DisposableEffect блока мора да позове onDispose, иначе ће доћи до грешке компајлирања. Међутим, програмери понекад покушавају да ово заобиђу постављањем onDispose-а у услов: if (condition) { onDispose { ... } }. Такав код ће се компајлирати, али onDispose неће бити регистрован ако услов није испуњен — ресурс никада неће бити ослобођен.
Друга грешка — коришћење DisposableEffect-а за асинхроне операције. Пошто је DisposableEffect синхрони, унутар њега се не могу писати позиви delay() или await(). Ако је потребна асинхрона иницијализација са накнадним чишћењем, користите комбинацију LaunchedEffect-а (за учитавање података) и DisposableEffect-а (за подешавање/чишћење домаћих ресурса), или користите засебан механизам са rememberCoroutineScope-ом.
Трећа грешка — креирање нових објеката унутар DisposableEffect-а без remember-а. Ако се унутар ефекта креирају објекти (сензор, listener, пријемник) при сваком позиву, а кључеви се често мењају, то доводи до прекомерног креирања објеката и сакупљања отпада. Боље је изместити креирање објеката у remember или remember { ... } изван DisposableEffect-а, а унутар ефекта само регистровати и отказивати их.
Често постављана питања
DisposableEffect ради синхроно и пружа onDispose за експлицитно чишћење ресурса. LaunchedEffect ради асинхроно у корутини и аутоматски је отказује при промени кључа или изласку из композиције. Ако ресурс захтева позив cleanup-методе (close, unregister, dispose) — користите DisposableEffect. Ако је операција suspend-функција — користите LaunchedEffect.
Да, onDispose је обавезан — Kotlin компајлер захтева његов позив унутар DisposableEffect блока. Ако се onDispose не позове, код се неће компајлирати. Ово је намерно урађено да би се спречила заборавност програмера и гарантовало да ће сваки отворени ресурс бити затворен при изласку из композиције.
Користите try-catch унутар DisposableEffect блока. Ако регистрација ресурса може да избаци изузетак (на пример, сензор није пронађен), обмотајте је у try и обрадите грешку у UI-ју путем засебног стања. onDispose се мора позвати без обзира на успешност иницијализације — поставите га у finally блок или на крај try-секције.
Не препоручује се. За Flow је боље користити LaunchedEffect са collectLatest или метод .collectAsState() са Lifecycle.repeatOnLifecycle. DisposableEffect не подржава suspend-функције, па би претплата на Flow унутар њега захтевала покретање засебне корутине кроз CoroutineScope, што компликује код и повећава ризик од цурења.
Нема ограничења, али се препоручује груписање повезаних ресурса у један DisposableEffect са више операција унутра и једним onDispose-ом. Ако су ресурси независни (на пример, сензор и BroadcastReceiver), боље их је раздвојити у засебне DisposableEffect-е са различитим кључевима — то поједностављује отклањање грешака и спречава нежељено поновно креирање свих ресурса при промени једног кључа.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође