- 개요
- 상태 및 사용 사례
- LaunchedEffect : Composable 범위에서 suspend function 실행
- LaunchedEffect
- 예제
- rememberCoroutineScope : Composition 인식 범위를 확보하여 Composable 외부에서 코루틴 실행
- rememberCoroutineScope
- 예제
- DisposableEffect : 정리가 필요한 Effect
- DisposableEffect
- 예제
- LaunchedEffect : Composable 범위에서 suspend function 실행
개요
SideEffect API는 Composable 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항이다. Composable의 수명 주기 및 속성으로 인해 SideEffect API는 없는 것이 좋지만, 필요한 경우가 발생한다. 예를 들어 스낵바를 표시하거나 특정 상태 조건에 따라 다른 화면으로 이동하는 등 일회성 이벤트를 트리거할 때 SideEffect API가 필요하다. 이런 작업들 또한 Composable의 수명 주기를 인식하는 관리된 환경 안에서 호출되어야 한다.
상태 및 사용 사례
LaunchedEffect : Composable 범위에서 suspend function 실행
Composable 범위 안에서 안전하게 suspend function을 호출하려면 LaunchedEffect를 사용한다. LaunchedEffect가 Composition을 시작하면 매개변수로 전달된 코드 블록으로 코루틴이 실행된다. LaunchedEffect가 Composition을 종료하면 코루틴이 취소된다. LaunchedEffect가 다른 키로 재구성되면 기존 코루틴이 취소되고 새 코루틴에서 새 suspend function이 실행된다.
LaunchedEffect
@Composable
@NonRestartableComposable
fun LaunchedEffect(vararg keys: Any?, block: suspend CoroutineScope.() -> Unit): Unit
Composable의 state가 변경되고 ReCpomposition이 발생될 때마다 LaunchedEffect 안에 있는 코루틴을 취소하고 실행한다면 상당히 비효율적이거나 이벤트 트리거가 발생했을 때 원하는 결과를 얻지 못할 수 있다. 그렇기 때문에 LaunchedEffect의 키 값이 변할 때만, 기존 코루틴을 취소하고 새 코루틴에서 suspend function을 실행한다. LaunchedEffect는 key 값을 vararg로 받기 때문에 여러 개의 키 값을 가질 수 있다. block이 코루틴인 이유는 LaunchedEffect가 해당 block을 취소하고 다시 실행할 수 있어야 하기 때문이라고 생각된다.
예제
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
// If the UI state contains an error, show snackbar
if (state.hasError) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(scaffoldState.snackbarHostState) {
// Show snackbar using a coroutine, when the coroutine is cancelled the
// snackbar will automatically dismiss. This coroutine will cancel whenever
// `state.hasError` is false, and only start when `state.hasError` is true
// (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
scaffoldState.snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
}
Scaffold(scaffoldState = scaffoldState) {
/* ... */
}
}
예제 코드는 상태에 오류가 포함되어 있으면 코루틴이 트리거되고 오류가 포함되어 있지 않으면 취소된다. LaunchedEffect 호출이 if문 안에 있으므로 문장이 거짓일 때 LaunchedEffect가 Composition에 있으면 삭제되며 코루틴이 취소된다. 예를 들어, 변경되는 어떤 값을 LaunchedEffect의 키 값으로 설정한다고 가정했을 때, 값이 변경될 때마다 LaunchedEffect에 전달한 코루틴이 취소되고 실행되기를 반복하는데, 그렇지 않은 경우 코루틴이 취소되지 않고 실행되기만을 반복하기 때문에 코루틴이 쌓이게 되고 결과적으로 문제가 생길 수 있게 된다.
참고자료 : https://kotlinworld.com/246
rememberCoroutineScope : Composition 인식 범위를 확보하여 Composable 외부에서 코루틴 실행
LaunchedEffect는 Composable 함수이므로 Composable 함수 안에서만 사용할 수 있다. Composable 외부에서 Composition을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 실행하려면 rememberCoroutineScope를 사용해야 한다. 또한, 호출되는 Composition 지점에 바인딩된 CoroutineScope를 반환하는 Composable 함수이다.
rememberCoroutineScope
@Composable
inline fun rememberCoroutineScope(
crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope
Composable 안에서 코루틴을 실행할 경우 ReComposition이 발생되면 정리되어야 하는 코루틴이 정리되지 못해서 코루틴이 계속 쌓일 수 있다. ReComposition은 꽤 자주 발생하는 동작이며, 코루틴이 계속 쌓이게 되는 것은 최악의 경우 앱이 비정상 종료될 수 있다. 따라서, Composable 안에서 코루틴을 생성한다면 코루틴은 Composable의 수명 주기를 따르는 CoroutinsScope를 반환해야 한다. 이를 위해서 rememberCoroutineScope 함수를 제공한다.
예제
@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { contentPadding ->
Column(Modifier.padding(contentPadding)) {
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
예제 코드는 버튼을 클릭하면 스낵바가 호출된다. 예를 들어, rememberCoroutineScope로 Composable의 수명주기에 바인딩된 Coroutine scope를 생성하고 해당 scope에서 코루틴을 호출하면 Composable의 수명주기가 끝날 때 scope 안에서 실행 중이던 코루틴이 취소된다. 만약 Activity에서 예제 코드의 MoviesScreen으로 activity의 lifecycleScope를 전달한 후 코루틴을 해당 lifecycleScope에서 구현하게 된다면 코루틴은 Activity의 수명 주기에 바인딩되기 때문에 결과적으로 ReComposition이 발생하더라도 코루틴이 취소되지 않고 쌓이게 되면서 문제가 발생할 수 있다.
참고자료 : https://kotlinworld.com/247
DisposableEffect : 정리가 필요한 Effect
키가 변경되거나 Composable이 Composition을 dispose한 후 정리해야 하는 Side Effect의 경우 DisposableEffect를 사용한다. Composable의 수명 주기에 맞춰 정리되어야 하는 작업이 있는 경우 해당 작업을 제거하기 위해서 DisposableEffect를 사용한다. DisposableEffect의 키가 변경되면 Composable이 현재 Effect를 삭제하고 Effect를 다시 호출하여 재설정해야 한다.
DisposableEffect
@Composable
@NonRestartableComposable
fun DisposableEffect(vararg keys: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult): Unit
특히 리스너와 같은 콜백 함수는 수명 주기가 끝나면 반드시 제거되어야 한다.
예제
@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)
}
}
/* Home screen content */
}
예제 코드는 수명 주기에 따라서 어떤 동작을 하는 코드이다. LaunchedEffect를 사용하여 onDispose를 제외하고 다른 부분은 위와 똑같은 코드를 구현할 수 있는데, DisposableEffect는 ReComposition이 발생하면 onDispose에서 해당 콜백을 remove 할 수 있지만, LaunchedEffect는 계속 생성되기 때문에 해당 코드 블록이 중첩되면서 문제가 발생할 수 있다.
참고자료 : https://kotlinworld.com/257
참고자료 :
https://developer.android.com/jetpack/compose/side-effects?hl=ko
'Mobile > Android(Kotlin)' 카테고리의 다른 글
[Android] Compose Column (Make a Vertical Layout) (0) | 2023.11.02 |
---|---|
[Android] Hilt @Binds와 @Provides 차이점. (abstract and object) (2) | 2023.10.14 |
[Android] Basic concept of Room Database and Query example (0) | 2023.08.24 |
[Android] Compose TabRow, Pager 예제. 화면 탭 버튼 예제 (0) | 2023.07.30 |
[Android] Android Preferences DataStore. ( + RxJava) (0) | 2023.06.08 |