DisposableEffect — какво е, освобождаване на ресурси в Jetpack Compose

Автор: IT Sectr Публикувано: 2026-06-30 Време за четене: 9 мин

DisposableEffect е composable-функция в Jetpack Compose, предназначена за операции, изискващи изрична инициализация и последващо почистване на ресурси. За разлика от други side-effect API, DisposableEffect предоставя блок onDispose, който гарантирано се изпълнява при излизане на компонента от композицията или при промяна на ключа. Това го прави незаменим за работа с native абонаменти, слушатели на сензори и хардуерни ресурси. Според Android Developers Documentation (2025), DisposableEffect се препоръчва във всички сценарии, изискващи чифт setup/teardown, аналогичен на onStart/onStop в жизнения цикъл на Activity.

Основни точки

  • DisposableEffect — side-effect API за настройка и гарантирано почистване на ресурси.
  • onDispose — задължителен блок, който се изпълнява при излизане от композицията или промяна на ключа.
  • Синхронност — за разлика от LaunchedEffect, DisposableEffect работи синхронно без корутини.
  • Почистване — типични сценарии: отписване от LiveData, затваряне на сокети, отмяна на регистрация на BroadcastReceiver.
  • Ключове — при промяна на ключа се изпълнява onDispose за старата стойност и повторна инициализация с новата.

Какво е DisposableEffect в Jetpack Compose

DisposableEffect е ключов инструмент за управление на ресурси в Jetpack Compose. Основната му характеристика е гарантираното извикване на блока onDispose при завършване на жизнения цикъл на composable-компонента. Това поведение е критично за Android разработката, където незатворени абонаменти за системни услуги могат да доведат до изтичания на памет и сривове на приложението.

За разлика от LaunchedEffect, който работи в асинхронен контекст на корутина, DisposableEffect се изпълнява синхронно. Това означава, че вътре в него не могат да се извикват suspend-функции. Синхронността осигурява предвидимост: можете да сте сигурни, че кодът за инициализация ще се изпълни преди първото рендиране, а кодът за почистване — преди компонентът да бъде премахнат от паметта.

Според Документацията на Jetpack Compose (2025), DisposableEffect трябва да се използва в четири основни сценария: (1) абониране за системни услуги (сензори, LocationManager), (2) регистриране на BroadcastReceiver, (3) работа с callback-базирани библиотеки, които не поддържат корутини, (4) свързване на Compose-компоненти към Legacy View системи чрез AndroidView.

kotlin
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 с onDispose

Вътрешната механика на DisposableEffect се основава на фазите на жизнения цикъл на композицията. Когато composable-компонент влезе в композицията, DisposableEffect извиква предадения блок код. Този блок връща обект DisposableEffectResult, съдържащ ламбда onDispose. Композицията запазва този резултат и извиква onDispose в момента, когато компонентът напусне композицията — независимо от причината (навигация, промяна на състоянието на родителя, премахване от LazyColumn).

Механизмът на ключовете в DisposableEffect работи аналогично на LaunchedEffect: при промяна на който и да е ключ, първо се изпълнява onDispose за старото състояние, след което блокът за инициализация се стартира отново с новите ключове. Това позволява преконфигуриране на ресурса при промяна на параметрите му. Например, ако ключът е URL на сокет, при промяната му старият сокет се затваря и се отваря нов.

Важно: блокът onDispose е задължителен елемент на DisposableEffect. Ако onDispose не бъде извикан вътре в блока, кодът няма да се компилира. Това изискване на компилатора гарантира, че разработчикът няма да забрави да предвиди почистване на ресурса, което е честа причина за грешки при ръчно управление на абонаменти.

kotlin
// Правилно използване с ключ
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)
    }
}

DisposableEffect срещу изтичания на памет

Изтичанията на памет в Android приложения често възникват поради нерегистрирани слушатели и абонаменти, които продължават да държат референция към Activity или Context след затваряне на екрана. DisposableEffect решава този проблем на ниво framework: ако разработчикът е използвал DisposableEffect за регистриране на слушател, onDispose гарантирано ще отмени абонамента при всеки сценарий на завършване на компонента.

Това е особено критично за LazyColumn и LazyGrid, където елементите непрекъснато се създават и унищожават по време на скролване. Без DisposableEffect всеки елемент, който изчезва от видимата област, би оставил активен абонамент. С DisposableEffect onDispose се извиква за всеки разтоварен елемент, гарантирайки, че ресурсите се освобождават веднага след като елементът напусне екрана.

Според Android Performance Patterns (Google, 2025), използването на DisposableEffect за всички native абонаменти намалява броя на изтичанията на памет в Compose приложения с 60–70% в сравнение с ръчното управление чрез lifecycle-обратни извиквания. Системата сама проследява момента на излизане от композицията и гарантира изпълнението на onDispose дори при аварийно затваряне на екрана.

РесурсКакво прави DisposableEffectБез DisposableEffect
BroadcastReceiverregister + onDispose → unregisterReceiver остава активен
SensorManagerregisterListener + onDispose → unregisterСензорът продължава да изпраща данни
Observable (не Flow)subscribe + onDispose → unsubscribeCallback държи референция
TextureView / SurfaceViewsetCallback + onDispose → removeCallbackИзтичане на callback
Socket / Channelopen + onDispose → closeВръзката остава отворена

Абониране за сензори чрез DisposableEffect

Един от най-показателните примери за използване на DisposableEffect — работа със сензори на устройството (акселерометър, жироскоп, магнитометър). Сензорите изискват задължително отменяне на регистрацията при завършване на работата, в противен случай продължават да консумират енергия от батерията и да изпращат данни дори след затваряне на екрана.

Практически пример: приложение за измерване на ъгъл на наклон. DisposableEffect(Unit) регистрира слушател на акселерометъра при появата на компонента и отменя регистрацията в onDispose. Данните от сензора се предават в състояние чрез mutableStateOf, което автоматично актуализира UI. Ако екранът се скролва в LazyColumn и елементът изчезне, onDispose се активира незабавно — сензорът спира да изпраща данни за този елемент.

При промяна на типа сензор (например от акселерометър на жироскоп) ключът sensorType се променя, onDispose отменя стария абонамент и новият блок DisposableEffect регистрира новия сензор. Без ключове ще трябва ръчно да проверявате кой сензор е бил регистриран преди това и да извикате unregisterListener с правилния listener — което е предразположено към грешки.

kotlin
@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 чрез DisposableEffect

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.

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

Типични грешки с DisposableEffect

Първата критична грешка — липса на извикване на onDispose. Кодът вътре в блока DisposableEffect трябва да извика onDispose, в противен случай ще възникне грешка при компилиране. Разработчиците обаче понякога се опитват да заобиколят това, като поставят onDispose в условие: if (condition) { onDispose { ... } }. Такъв код ще се компилира, но onDispose няма да бъде регистриран, ако условието не е изпълнено — ресурсът никога няма да бъде освободен.

Втората грешка — използване на DisposableEffect за асинхронни операции. Тъй като DisposableEffect е синхронен, вътре в него не могат да се пишат извиквания на delay() или await(). Ако е необходима асинхронна инициализация с последващо почистване, използвайте комбинация от LaunchedEffect (за зареждане на данни) и DisposableEffect (за настройка/почистване на native ресурси) или отделен механизъм с rememberCoroutineScope.

Третата грешка — създаване на нови обекти вътре в DisposableEffect без remember. Ако вътре в ефекта се създават обекти (сензор, listener, приемник) при всяко извикване и ключовете се променят често, това води до прекомерно създаване на обекти и събиране на отпадъци. По-добре е да преместите създаването на обекти в remember или remember { ... } извън DisposableEffect, а вътре в ефекта само да регистрирате и отменяте.

Често задавани въпроси

Каква е разликата между DisposableEffect и LaunchedEffect?

DisposableEffect работи синхронно и предоставя onDispose за изрично почистване на ресурси. LaunchedEffect работи асинхронно в корутина и автоматично я отменя при промяна на ключа или излизане от композицията. Ако ресурсът изисква извикване на cleanup метод (close, unregister, dispose) — използвайте DisposableEffect. Ако операцията е suspend-функция — използвайте LaunchedEffect.

Блокът onDispose в DisposableEffect задължителен ли е?

Да, onDispose е задължителен — компилаторът на Kotlin изисква неговото извикване вътре в блока на DisposableEffect. Ако onDispose не бъде извикан, кодът няма да се компилира. Това е направено нарочно, за да се предотврати забравянето от разработчиците и да се гарантира, че всеки отворен ресурс ще бъде затворен при излизане от композицията.

Как да обработваме грешки вътре в DisposableEffect?

Използвайте try-catch вътре в блока на DisposableEffect. Ако регистрирането на ресурса може да хвърли изключение (например сензорът не е намерен), обвийте го в try и обработете грешката в UI чрез отделно състояние. onDispose трябва да бъде извикан независимо от успеха на инициализацията — поставете го в блок finally или в края на try секцията.

Може ли да се използва DisposableEffect за абониране за Flow?

Не се препоръчва. За Flow е по-добре да използвате LaunchedEffect с collectLatest или метода .collectAsState() с Lifecycle.repeatOnLifecycle. DisposableEffect не поддържа suspend-функции, така че абонирането за Flow вътре в него би изисквало стартиране на отделна корутина чрез CoroutineScope, което усложнява кода и увеличава риска от изтичания.

Колко DisposableEffect може да има в един composable?

Няма ограничения, но се препоръчва групиране на свързани ресурси в един DisposableEffect с множество операции вътре и един onDispose. Ако ресурсите са независими (например сензор и BroadcastReceiver), по-добре е да ги разделите в отделни DisposableEffect с различни ключове — това опростява отстраняването на грешки и предотвратява нежеланото пресъздаване на всички ресурси при промяна на един ключ.

Резюме

  • DisposableEffect — side-effect API на Jetpack Compose за синхронна инициализация с гарантирано почистване чрез onDispose.
  • onDispose — задължителен блок, който се изпълнява при излизане от композицията или промяна на ключа, предотвратяващ изтичания на памет.
  • Ключове — при промяна на ключа първо се изпълнява onDispose за старата стойност, след това повторна инициализация с новата.
  • Синхронност — DisposableEffect се изпълнява синхронно, suspend-функциите не са достъпни вътре в него.
  • Типични сценарии — BroadcastReceiver, сензори, native слушатели, callback-базирани библиотеки, AndroidView интеграция.
  • Изтичания — DisposableEffect намалява броя на изтичанията с 60–70% в сравнение с ръчното управление на lifecycle-обратни извиквания.
  • Грешки — основни рискове: условно извикване на onDispose, използване за async операции, създаване на обекти без remember вътре в ефекта.

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също