안녕하세요🙌🏻
Mash-up Android 12기 백다연입니다.
Jetpack Compose를 프로젝트에 도입해보면서 조금 더 깊게 공부해보고 싶다는 생각을 갖게 되어 Compose Side Effect 라는 주제로 글을 작성해보려고 합니다! Side Effect가 무엇인지, 어떻게 처리하는 지 등 간단하게 소개해보도록 하겠습니다.
Composable을 사용할 때 여러 Composable을 겹쳐서 사용합니다. 이 경우 시스템은 각 Composable에 대한 LifeCycle을 만들게 됩니다. 또한, 기본적으로 Composable은 바깥쪽에서 안쪽으로 State를 내려줍니다.
하지만,
⇒ 단방향이 아닌 양방향 의존성으로 Effect가 생기며 이를 Side Effect
라고 부릅니다.
Composable은 Side Effect가 없는 것이 좋으나, 앱 상태를 변경해야 하는 경우 Side Effect를 예측 가능한 방식으로 실행되도록 Effect API를 사용해야 합니다.
@Composable
fun LaunchedEffect(
key1 : Any?,
block : suspend CorountineScope.() -> Unit
) {}
LaunchedEffect는 key라 불리는 기준가을 두어 key가 바뀔 때만 LaunchedEffect의 supsend fun을 취소하고 재실행합니다.
⇒recomposition은 Composable의 State가 바뀔 때마다 일어나므로, 만약 recomposition이 일어날 때마다 이전 LaunchedEffect가 취소되고 다시 수행된다면 매우 비효율적이기 때문에 이를 해결하기 위해!
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
if (state.hasError) {
LaunchedEffect(scaffoldState.snackbarHostState) {
scaffoldState.snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
}
Scaffold(scaffoldState = scaffoldState) {
/* ... */
}
}
해당 코드는 상태가 오류일 때 스낵바를 보여주는 코드입니다. 코루틴이 취소되면 스낵바는 dismiss됩니다. 즉, 상태에 오류가 포함되어 있으면 코루틴이 실행되고 오류가 포함되어 있지 않으면 취소됩니다.
그렇다면 LaunchedEffect의 block을 두개 이상의 변수에 의해 재실행 해야 할 때는 어떻게 해야할까요?
@Composable
fun LaunchedEffect(
key1 : Any?,
key2 : Any?,
block : suspend CorountineScope.() -> Unit
) {}
@Composable
fun LaunchedEffect(
vararg : Any?,
block : suspend CorountineScope.() -> Unit
) {}
바로 key값을 개수만큼 추가해주면 됩니다! vararg를 사용하여 Key값을 무제한으로 줄 수 있다는 것도 기억합시다.
@Composable
fun DisposableEffect(
key1 : Any?,
effect : DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
⇒ key값은 DisposableEffect가 재수행되는 것을 결정하는 파라미터,
**Effect** 람다식은 DisposableEffectResult를 return 값으로 하는 식입니다.
이제 실제로 어떻게 사용하는지 알아보도록 하겠습니다.
DisposableEffect(key) {
//Composable이 제거될 때 Dispose 되어야 하는 효과 초기화
onDispose {
//Composable이 Dispose될 때 호출되어 Dispose 되어야 하는 효과 제거
}
}
위 코드에서는 effect 블록은 처음에는 초기화 로직만 수행하고 이후에는 key값이 바뀔 때마다 onDispose블록을 호출한 후 초기화 로직을 다시 호출합니다.
여기서 주의할 점은 onDispose 블록의 리턴 값이 바로 DisposableEffect여서 onDispose 블록은 effect 람다식의 맨 마지막에 꼭 와야합니다.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// Safely update the current lambdas when a new one is provided
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
// If `lifecycleOwner` changes, dispose and reset the effect
DisposableEffect(lifecycleOwner) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
예시
Activity의 onStart에서 시작되어 onStop에서 끝나야 하는 경우(백그라운드에서는 실행이 필요없는 경우)가 있다고 생각해봅시다. rememberUpdatedStateon
를 이용하여 onStart와 onStop에 대한 State를 저장하고 해당 라이프사이클에 맞는 case를 정의해줍니다.
그리고 가장 중요한 onDispose에 observer를 제거해주는 코드를 작성해줍니다.
만약, DisposableEffect
가 아닌 LaunchedEffect
를 사용하여 구현했다면 어떤 것이 문제일까요?
LaunchedEffect를 사용했을 경우 lifecycle이 바뀔 때마다 lifecycle Owner의 lifecycle에 붙는데 이 observer가 정리되는 부분이 없기때문에 observer은 계속해서 이전 lifecyclerOwner에 붙어 있을 것입니다. 즉 , onDispose를 이용하여 lifecylce이 바뀔 때 새로운 observer가 livecycle에 붙어 변화를 구독하고 composable이 제거될 때 observer 또한 정리되는 것입니다.
@Composable
fun BackHandler(
backDispatcher: OnBackPressedDispatcher,
enabled: Boolean = true, // Whether back events should be intercepted or not
onBack: () -> Unit
) {
val backCallback = remember { /* ... */ }
// On every successful composition, update the callback with the `enabled` value
// to tell `backCallback` whether back events should be intercepted or not
SideEffect {
backCallback.isEnabled = enabled
}
}
위 예시처럼 BackHandler 함수와 같이 콜백을 사용 설정해야 하는지 전달하려면 SidEffect를 사용하여 값을 업데이트 할 수 있습니다.
추가적으로 Compose는 위 3가지와 함께 사용할 수 있는 여러가지 CoroutineScope과 State관련 함수를 제공하는데요,
도 같이 알아두고 공부하면 좋을 것 같습니다!!!
🧚🏻 지금까지 Composable의 Side Effect와 이를 처리하는 방법에 대해서 알아보았습니다.!!
12기 프로젝트에서 컴포즈를 처음으로 사용해보았는데요, 사실 깊이 있게 공부하지 않고 바로 적용해서 어려움이 있었고 다양하게 사용해보며 공부해봐야겠다는 생각을 하게되었습니다~!
🚗다같이 컴포즈 마스터 해봅시다! 🚗
참고
https://developer.android.com/jetpack/compose/side-effects?hl=ko