Compose를 사용하면서 LaunchedEffect, DisposableEffect와 같은 일명 부수 효과
함수들을 사용할 일들이 꽤 많다. 이에 대해 정리의 필요성을 느끼고, 공식문서를 읽고 학습한 내용을 바탕으로 정리하였다.
부수효과는 Composable 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항이다.
부수 효과는 되도록 없으면 좋으나, 컴포저블의 범위 밖에서 상태의 변경이 필요한 상황이면 예측 가능한 방식으로 실행되도록 Effect API
를 사용해야 한다.
Effect API에서의 작업은 UI와 관련되어 있어야 하며, Compose의 기본적인 단방향 흐름을 해치면 안된다.
기본적으로 반응형 UI는 비동기로 진행되며 Compose에서는 이를 콜백을 사용하지 않고 코루틴
을 이용한다.
LaunchedEffect
가 컴포지션에 진입하면, 즉 컴포지션이 이루어지거나 Composable 함수가 최초로 호출될 때, 컴포지션의 CoroutineContext로 코루틴 블락을 시작한다.
만약 LaunchedEffect 함수의 인자인 key
값이 변경되면, 현재 실행중인 코루틴은 취소되고 재시작된다. 해당 Effect가 컴포지션을 벗어나면 코루틴은 자동 취소된다. 키 값의 변화에 따라 중단 함수를 호출하거나 컴포저블 내부에서 외부의 값을 구독하면서 중단 함수를 호출해야 하는 경우에는 LaunchedEffect
를 사용한다.
내부적으로 remember(key1)
블락을 사용중이며, 이는 다음과 같이 동작한다.
LaunchedEffect
에 키 값을 여러 개 지정할 수 있으며, 여러 개 값들 중 하나라도 변경되면 코루틴이 재실행된다.
LaunchedEffect(key){
progress = 0f
while (progress < 1f){
progress += 1f / seconds
delay(1000) // 중단 함수
if (progress >= 1f) onProgressCompleted()
}
}
LaunchedEffect(Unit){
viewModel.moveToSetting.collectLatest // 중단 함수 {
popBackStack(it)
}
}
첫번째 코드 블락은 키 값이 변함에 따라 while 문 내부에서 실행 중인 코루틴이 취소되고, 이에 따라 코루틴이 재시작되며 progress값은 0으로 할당된다
두번째 코드 블락은 키 값으로 Unit을 설정하였다. 이렇게 변하지 않는 값을 할당하면, 코루틴의 수명주기는 해당 컴포저블의 수명주기와 동일하게 된다. 이런 방식은 ViewModel에서 특정 값의 변화를 구독(collectLatest)하는 경우 유용하다.
DisposableEffect
는 컴포저블이 컴포지션을 종료한 후 정리 해야 하는 부수 효과가 있는 경우 사용한다. DisposableEffect에 Key값을 설정하면, 해당 키 값이 변경되면 내부의 onDispose
절이 실행되고, 해당 Effect를 다시 호출하여 재설정한다.
해당 Effect는 컴포저블의 수명주기에 맞게 정리되어야 할 리스너가 있거나 처리해야 할 이벤트가 있는 경우 사용한다.
@Composable
fun DisposableEffectWithLifeCycle(
onResume: () -> Unit,
onPause: () -> Unit,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
// Safely update the current lambdas when a new one is provided
val currentOnResume by rememberUpdatedState(onResume)
val currentOnPause by rememberUpdatedState(onPause)
// If `lifecycleOwner` changes, dispose and reset the effect
DisposableEffect(lifecycleOwner) {
// Create an observer that triggers our remembered callbacks
// for lifecycle events
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
currentOnResume()
}
Lifecycle.Event.ON_PAUSE -> {
currentOnPause()
}
else -> {}
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
DisposableEffect는 onDispose
절을 코드 블록의 최종 문장으로 포함해야 한다. 그렇지 않으면 빌드 오류가 표시된다.
SideEffect
는 Compose의 상태를 Compose에서 관리하지 않는 객체와 공유하려면 해당 Effect를 사용한다. 이는 리컴포지션 성공마다 호출된다.
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
FirebaseAnalytics 라이브러리를 사용하여 유저의 타입을 설정하는 경우(Compose 에서 관리하지 않는 객체와 공유) 해당 효과를 사용하여 처리할 수 있다.
LaunchedEffect
와 같이 컴포저블 내에서 중단 함수를 사용하는 경우, 코루틴 스코프를 정의하고 launch 구문을 통해 중단 함수를 호출하게 된다.
코루틴은 구조화된 동시성이라는 특징을 가지고 있으므로, 그 수명주기는 CoroutineScope
로 제한되어 있다. 이 말은 코루틴이 그 수명주기가 종료되면 취소되어야 한다는 것을 의미한다. 따라서 컴포저블 내에서 사용하는 코루틴의 수명주기는 컴포저블의 수명주기와 일치해야 한다. 즉, 컴포저블이 컴포지션을 벗어나는 순간, 해당 코루틴은 자동으로 취소되어야 한다.
만약 Composable 내부에서 Activity의 lifecycleScope를 사용한다면, 해당 스코프로 지정된 코루틴은 컴포지션이 종료되어도 취소되지 않는다.
Compose에서는 rememberCoroutineScope
라는 api를 제공하고 있으며, 이 CoroutineScope는 Composable의 생명주기를 따른다.
컴포저블이 파괴될 때 자동으로 취소되어야 하는 코루틴을 생성해야 한다면, rememberCoroutineScope
를 사용하자.
https://developer.android.com/jetpack/compose/side-effects?hl=ko