등록과 해제가 짝을 이루는 효과.
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) analytics.startSession()
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { // ★ 필수
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
LaunchedEffect와 무엇이 다른가
- LaunchedEffect — 코루틴을 띄운다. 취소가 곧 정리다
- DisposableEffect — 코루틴이 아니다. 정리를 직접 적어야 한다
리스너 등록 · 콜백 구독 · 센서 · 브로드캐스트처럼 "removeXxx 를 불러야 하는 것" 이 전부 여기 온다
실전 — 시스템 UI 제어
DisposableEffect(Unit) {
val window = (context as Activity).window
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
onDispose { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) }
}
동영상 재생 화면에서 화면 꺼짐을 막고, 떠나면 되돌린다 onDispose 를 빠뜨리면 앱 전체에서 화면이 안 꺼진다 — 배터리 사고
센서 구독
DisposableEffect(Unit) {
val listener = object : SensorEventListener {
override fun onSensorChanged(e: SensorEvent) { value = e.values[0] }
override fun onAccuracyChanged(s: Sensor?, a: Int) {}
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
onDispose { sensorManager.unregisterListener(listener) }
}
key가 바뀌면
onDispose 를 먼저 실행하고 → 블록을 다시 실행한다 = 항상 '해제 후 재등록' 순서가 보장된다
key 를 안 주면(Unit) 컴포지션 진입/이탈에만 반응한다
면접 함정
- ❌
onDispose를 생략한다 → 컴파일 에러다. 마지막 문장이 반드시onDispose여야 한다는 점이 설계로 강제돼 있다. - ❌
DisposableEffect안에서 suspend 함수를 부른다 → 코루틴 스코프가 아니다. 그건LaunchedEffect다.
콜백 기반 API를 Flow로 감쌀 때
// callbackFlow 는 DisposableEffect 와 같은 발상이다 — 등록하고 반드시 해제한다
fun SensorManager.readings(sensor: Sensor): Flow<FloatArray> = callbackFlow {
val listener = object : SensorEventListener {
override fun onSensorChanged(e: SensorEvent) { trySend(e.values.clone()) }
override fun onAccuracyChanged(s: Sensor?, a: Int) {}
}
registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
awaitClose { unregisterListener(listener) } // ★ onDispose 와 같은 역할
}
awaitClose 를 빠뜨리면 컴파일 경고가 나고 리스너가 영영 남는다
key 없이 쓸 때 흔한 실수
// ❌ url 이 바뀌어도 옛 url 로 구독한 채다
DisposableEffect(Unit) {
val sub = client.subscribe(url) { ... }
onDispose { sub.cancel() }
}
// ✅ url 이 바뀌면 해제 후 재구독
DisposableEffect(url) { ... }
생명주기와 함께 쓰는 정석
val owner = LocalLifecycleOwner.current
DisposableEffect(owner) {
val obs = LifecycleEventObserver { _, e ->
when (e) {
Lifecycle.Event.ON_RESUME -> player.play()
Lifecycle.Event.ON_PAUSE -> player.pause()
else -> Unit
}
}
owner.lifecycle.addObserver(obs)
onDispose { owner.lifecycle.removeObserver(obs); player.release() }
}
컴포지션 이탈(onDispose) 과 생명주기 이벤트를 둘 다 다뤄야 한다 화면을 떠나는 것과 앱이 백그라운드로 가는 것은 다른 사건이다