Disposable Effect란?
DisposableEffect란 Composable이 Dispose된 후에 정리해야 할 Side Effect가 있는 경우에 사용되는 Effect이다. 자세히 이야기 하면 Composable의 Lifecycle에 맞춰 정리되어야 하는 리스너나 작업이 있는 경우에 리스너나 작업을 제거하기 위해 사용되는 Effect가 바로 DisposableEffect이다.
안드로이드에서는 Lifecycle에 따라 Side Effect(부수 효과)를 발생시킨 다음 정리되어야 하는 부분이 많다. 이런 부분에서 제대로 Side Effect에 대한 정리를 하지 않으면 제어권을 잃어 메모리에 leak이 생기거나 예측하지 못한 결과나 나올 수 있다.
Disposable Effect 사용 방법
DisposableEffect는 key값과 effect 람다식을 인자로 받는다. key값은 Disposable Effect가 재수행되는 것을 결정하는 파라미터이며, effect람다식은 DisposableEffectResult를 return 값으로 하는 식이다.
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
자 위의 fun이 어떻게 실제로 사용되는지 살펴보자. 먼저 Disposable Effect를 사용하는 형태는 다음과 같다.
DisposableEffect(key) {
//Composable이 제거될 때 Dispose 되어야 하는 효과 초기화
onDispose {
//Composable이 Dispose될 때 호출되어 Dispose 되어야 하는 효과 제거
}
}
위 코드에서 effect 블록은 처음에는 초기화 로직만 수행하고 이후에는 key값이 바뀔 때마다 onDispose 블록을 호출한 후 초기화 로직을 다시 호출한다.
onDispose 블록의 리턴 값이 바로 DisposableEffect여서 onDispose 블록은 effect 람다식의 맨 마지막에 꼭 와야 한다.
inline fun onDispose(
crossinline onDisposeEffect: () -> Unit
): DisposableEffectResult = object : DisposableEffectResult {
override fun dispose() {
onDisposeEffect()
}
}
Disposable Effect 사용 예시
사용자의 사용패턴 분석을 위한 로깅을 생각해보자. 분석을 위한 로깅은 Activity의 onStart에서 시작되어 onStop에서 끝나야 한다. 그렇지 않으면 앱이 백그라운드에 내려가서도 로깅 작업이 지속될 것이며, 이는 로깅의 신뢰도를 저하시킨다. 따라서 로깅을 위해서는 다음의 LifecycleEventObserver을 Screen Composable의 lifecycle에 붙여야 한다.
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
startLoggingOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
stopLoggingOnStop()
}
}
위의 observer을 Lifecycle에 붙이기 위해서 LaunchedEffect를 사용하면 다음과 같이 코드를 만들 수 있다. 하지만 이 코드는 문제를 가지고 있다 한 번 어떤 것이 문제인지 맞혀보자.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
_onStartLogging: () -> Unit,
_onStopLogging: () -> Unit
) {
val startLoggingOnStart by rememberUpdatedState(_onStartLogging)
val stopLoggingOnStop by rememberUpdatedState(_onStopLogging)
LaunchedEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
startLoggingOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
stopLoggingOnStop()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
println("KotlinWorld >> Observer Attached")
}
}
위 코드에서 문제는 바로 observer가 lifecycleOwner가 바뀔 때마다 lifecycleOwner의 lifecycle에 붙는데 이 observer가 정리되는 부분이 없는 것이다. 만약 observer가 정리되지 않는다면 저 observer은 계속해서 이전 lifecycleOwner에 붙어 있을 것이다.
위와 같이 정리되어야 하는 Effect(observer)가 있는 경우에는 바로 Disposable Effect를 사용할 수 있다. lifecycle이 바뀔 때 새로운 옵저버가 lifecycle에 붙어 변화를 구독하고, composable이 제거될 때 observer 또한 정리되는 것이다.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
_onStartLogging: () -> Unit,
_onStopLogging: () -> Unit
) {
val startLoggingOnStart by rememberUpdatedState(_onStartLogging)
val stopLoggingOnStop by rememberUpdatedState(_onStopLogging)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
startLoggingOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
stopLoggingOnStop()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
println("KotlinWorld >> Observer Attached")
onDispose {
// Composable이 dispose될 때 observer을 제거함
lifecycleOwner.lifecycle.removeObserver(observer)
println("KotlinWorld >> Observer Removed")
}
}
}
사용 결과 화면
위에서 만든 HomeScrreen을 다음의 코드를 이용해 실행해보자.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HomeScreen(
_onStartLogging = {
println("KotlinWorld >> Logging Started")
},
_onStopLogging = {
println("KotlinWorld >> Logging Stopped")
}
)
}
}
}
우리는 다음과 같은 순서로 앱을 실행한다.
1. 앱 실행 : onCreate후 Composition Start되어 observer가 lifecycle에 Attach되고 logging이 시작됨
2. 홈 버튼 누르기 : onStop 수행 되어 logging이 중지됨
3. 다시 앱 실행 : onStart 수행되어 logging이 다시 시작됨
4. 백 버튼 누르기 : onStop이 수행되어 로깅이 중지되고, onDestroy가 호출되기 전 Composable이 onDispose 되어 observer가 Remove됨