DisposableEffect е composable-функция в Jetpack Compose, предназначена за операции, изискващи изрична инициализация и последващо почистване на ресурси. За разлика от други side-effect API, DisposableEffect предоставя блок onDispose, който гарантирано се изпълнява при излизане на компонента от композицията или при промяна на ключа. Това го прави незаменим за работа с native абонаменти, слушатели на сензори и хардуерни ресурси. Според 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 за всички native абонаменти намалява броя на изтичанията на памет в 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 (за настройка/почистване на native ресурси) или отделен механизъм с 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също