DisposableEffect — это composable-функция в Jetpack Compose, предназначенная для операций, требующих явной инициализации и последующей очистки ресурсов. В отличие от других side-effect API, DisposableEffect предоставляет блок onDispose, который гарантированно выполняется при выходе компонента из композиции или при смене ключа. Это делает его незаменимым для работы с нативными подписками, слушателями сенсоров и аппаратными ресурсами. По данным Android Developers Documentation (2025), DisposableEffect рекомендуется использовать во всех сценариях, где требуется пара setup/teardown, аналогичная onStart/onStop в Activity lifecycle.
Главное
DisposableEffect — это ключевой инструмент для управления ресурсами в Jetpack Compose. Его главная особенность — гарантированный вызов блока onDispose при завершении жизненного цикла composable-компонента. Это поведение критически важно для Android-разработки, где незакрытые подписки на системные сервисы могут привести к утечкам памяти и аварийным завершениям приложения.
В отличие от LaunchedEffect, который работает в асинхронном контексте корутины, DisposableEffect выполняется синхронно. Это означает, что внутри него нельзя вызывать suspend-функции. Синхронность обеспечивает предсказуемость: вы можете быть уверены, что код инициализации выполнится до первой отрисовки, а код очистки — до того, как компонент будет удалён из памяти.
По данным Документации Jetpack Compose (2025), DisposableEffect следует использовать в четырёх основных сценариях: (1) подписка на системные сервисы (сенсоры, LocationManager), (2) регистрация BroadcastReceiver, (3) работа с callback-based библиотеками, не поддерживающими корутины, (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("Sensor: $value")
}
Внутренняя механика DisposableEffect основана на фазах жизненного цикла композиции. Когда composable-компонент входит в композицию, DisposableEffect вызывает переданный блок кода. Этот блок возвращает объект DisposableEffectResult, содержащий лямбду onDispose. Композиция сохраняет этот результат и вызывает onDispose в момент, когда компонент покидает композицию — независимо от причины (навигация, изменение состояния родителя, удаление из LazyColumn).
Механизм ключей в DisposableEffect работает аналогично LaunchedEffect: при изменении любого ключа сначала выполняется onDispose для старого состояния, затем блок инициализации запускается заново с новыми ключами. Это позволяет переконфигурировать ресурс при изменении его параметров. Например, если ключ — это URL сокета, при его изменении старый сокет закрывается и открывается новый.
Важно: блок onDispose — обязательный элемент DisposableEffect. Если не вызвать onDispose внутри блока, код не скомпилируется. Это требование компилятора гарантирует, что разработчик не забудет предусмотреть очистку ресурса, что является частой причиной ошибок при ручном управлении подписками.
// Correct usage with a key
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
// Multiple resources in one 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% по сравнению с ручным управлением через 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("Value: $sensorValue")
}
BroadcastReceiver — классический пример API, требующий обязательной пары register / unregister. В Compose-приложении DisposableEffect идеально подходит для регистрации ресивера на время жизни конкретного экрана. При входе на экран регистрируется BroadcastReceiver с нужным IntentFilter, при выходе — автоматически отменяется в onDispose.
Типовой сценарий — мониторинг состояния сети. DisposableEffect регистрирует ресивер на ConnectivityManager, который оповещает об изменениях сетевого подключения. При изменении статуса (WiFi / мобильные данные / без сети) обновляется состояние composable, и UI отображает соответствующий индикатор. Когда экран закрывается, onDispose гарантированно отменяет регистрацию — даже если приложение уходит в background.
Для ресиверов с 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, receiver) при каждом вызове, а ключи часто меняются, это приводит к избыточному созданию объектов и сборке мусора. Лучше вынести создание объектов в 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также