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-компонентів до легатарних 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 вирішує цю проблему на рівні фреймворку: якщо розробник використав DisposableEffect для реєстрації слухача, onDispose гарантовано скасує підписку при будь-якому сценарії завершення компонента.
Особливо критично це для LazyColumn та LazyGrid, де елементи постійно створюються та знищуються під час прокрутування. Без DisposableEffect кожен елемент, що зникає з видимої області, залишав би активну підписку. З DisposableEffect onDispose викликається для кожного вивантаженого елемента, гарантуючи, що ресурси звільняються одразу після того, як елемент покидає екран.
Згідно з Android Performance Patterns (Google, 2025), використання DisposableEffect для всіх нативних підписок скорочує кількість витоків пам’яті в Compose-програмах на 60–70% порівняно з ручним керуванням через колбеки життєвого циклу. Система сама відстежує момент виходу компонента з композиції та гарантує виконання onDispose навіть при екстреному закритті екрану.
| Ресурс | Що робить DisposableEffect | Без DisposableEffect |
|---|---|---|
| BroadcastReceiver | register + onDispose → unregister | Приймач залишається активним |
| SensorManager | registerListener + onDispose → unregisterListener | Сенсор продовжує надсилати дані |
| 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 з правильним слухачем — що чревато помилками.
@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. Якщо об’єкти (сенсор, слухач, приймач) створюються всередині ефекту при кожному викликові, а ключі часто змінюються, це призводить до надмірного створення об’єктів та збирання сміття. Краще винести створення об’єктів у remember або remember { ... } за межі DisposableEffect, а всередині ефекту лише реєструвати та скасовувати їх.
Часто задавані питання
DisposableEffect працює синхронно та надає onDispose для явного очищення ресурсів. LaunchedEffect працює асинхронно в корутині та автоматично скасовує її при зміні ключа або виході з композиції. Якщо ресурс вимагає виклику методу очищення (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), краще розділити їх на окремі DisposableEffects з різними ключами — це спрощує налагодження та запобігає небажаному перестворенню всіх ресурсів при зміні одного ключа.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також