안드로이드 시스템·Compose 용어 사전
ComposeonDispose

DisposableEffect

등록/해제가 짝을 이루는 효과. 떠날 때 onDispose에서 반드시 정리한다.

등록과 해제가 짝을 이루는 효과.

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) 과 생명주기 이벤트를 둘 다 다뤄야 한다 화면을 떠나는 것과 앱이 백그라운드로 가는 것은 다른 사건이다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Compose 상태·side-effect — remember·LaunchedEffect