DisposableEffect — звільнення ресурсів в Jetpack Compose

Автор: IT Sectr Опубліковано: 2026-06-30 Час читання: 9 хв

DisposableEffect — це composable-функція в Jetpack Compose, призначена для операцій, які потребують явної ініціалізації та подальшого очищення ресурсів. На відміну від інших side-effect API, DisposableEffect надає блок onDispose, який гарантовано виконується при виході компонента з композиції або при зміні ключа. Це робить його незамінним для роботи з нативними підписками, слухачами сенсорів та апаратними ресурсами. Згідно з 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-компонентів до легатарних 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 вирішує цю проблему на рівні фреймворку: якщо розробник використав DisposableEffect для реєстрації слухача, onDispose гарантовано скасує підписку при будь-якому сценарії завершення компонента.

Особливо критично це для LazyColumn та LazyGrid, де елементи постійно створюються та знищуються під час прокрутування. Без DisposableEffect кожен елемент, що зникає з видимої області, залишав би активну підписку. З DisposableEffect onDispose викликається для кожного вивантаженого елемента, гарантуючи, що ресурси звільняються одразу після того, як елемент покидає екран.

Згідно з Android Performance Patterns (Google, 2025), використання DisposableEffect для всіх нативних підписок скорочує кількість витоків пам’яті в Compose-програмах на 60–70% порівняно з ручним керуванням через колбеки життєвого циклу. Система сама відстежує момент виходу компонента з композиції та гарантує виконання onDispose навіть при екстреному закритті екрану.

РесурсЩо робить DisposableEffectБез DisposableEffect
BroadcastReceiverregister + onDispose → unregisterПриймач залишається активним
SensorManagerregisterListener + onDispose → unregisterListenerСенсор продовжує надсилати дані
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 з правильним слухачем — що чревато помилками.

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 (для налаштування/очищення нативних ресурсів), або використовуйте окремий механізм з rememberCoroutineScope.

Третя помилка — створення нових об’єктів всередині DisposableEffect без remember. Якщо об’єкти (сенсор, слухач, приймач) створюються всередині ефекту при кожному викликові, а ключі часто змінюються, це призводить до надмірного створення об’єктів та збирання сміття. Краще винести створення об’єктів у remember або remember { ... } за межі DisposableEffect, а всередині ефекту лише реєструвати та скасовувати їх.

Часто задавані питання

У чому різниця між DisposableEffect та LaunchedEffect?

DisposableEffect працює синхронно та надає onDispose для явного очищення ресурсів. LaunchedEffect працює асинхронно в корутині та автоматично скасовує її при зміні ключа або виході з композиції. Якщо ресурс вимагає виклику методу очищення (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), краще розділити їх на окремі DisposableEffects з різними ключами — це спрощує налагодження та запобігає небажаному перестворенню всіх ресурсів при зміні одного ключа.

Підсумки

  • DisposableEffect — side-effect API Jetpack Compose для синхронної ініціалізації з гарантованим очищенням через onDispose.
  • onDispose — обов’язковий блок, що виконується при виході з композиції або зміні ключа, що запобігає витокам пам’яті.
  • Ключі — при зміні ключа спочатку виконується onDispose для старого значення, потім повторна ініціалізація з новим.
  • Синхронність — DisposableEffect виконується синхронно; suspend-функції всередині нього недоступні.
  • Типові сценарії — BroadcastReceiver, сенсори, нативні слухачі, callback-базовані бібліотеки, інтеграція AndroidView.
  • Витоки — DisposableEffect скорочує кількість витоків на 60–70% порівняно з ручним керуванням колбеками життєвого циклу.
  • Помилки — головні ризики: умовний виклик onDispose, використання для async-операцій, створення об’єктів без remember всередині ефекту.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також