Mobile/Android(Kotlin)

[Android] Dive into Compose Side Effect API. Compose Effect Coroutine Logic.

개발왕 금골드 2023. 10. 14. 18:09
반응형
  • 개요
  • 상태 및 사용 사례
    • LaunchedEffect : Composable 범위에서 suspend function 실행
      • LaunchedEffect
      • 예제
    • rememberCoroutineScope : Composition 인식 범위를 확보하여 Composable 외부에서 코루틴 실행
      • rememberCoroutineScope
      • 예제
    • DisposableEffect : 정리가 필요한 Effect
      • DisposableEffect
      • 예제

개요

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

 

[Compose Side Effect] 1. LaunchedEffect 를 이용한 suspend fun 실행

LaunchedEffect 살펴보기 LaunchedEffect는 Composable에서 컴포지션이 일어날 때 suspend fun을 실행해주는 Composable이다. @Composable fun LaunchedEffect( key1: Any?, block: suspend CoroutineScope.() -> Unit ) { .. } 리컴포지션은 C

kotlinworld.com

 

 

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

 

[Compose Side Effect] rememberCoroutineScope을 이용해 Composable의 생명주기에 맞춰 코루틴 수행하기

Composable에서 올바른 CoroutineScope을 선택하는 것이 중요한 이유 Composable 내부에서 코루틴을 수행할 경우 Composable에 대한 Recomposition이 일어날 때 정리되어야 하는 Coroutine이 정리가 안된 상태로 계

kotlinworld.com

 

 

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

 

[Compose Side Effect] Disposable Effect 란 무엇인가?

Disposable Effect란? DisposableEffect란 Composable이 Dispose된 후에 정리해야 할 Side Effect가 있는 경우에 사용되는 Effect이다. 자세히 이야기 하면 Composable의 Lifecycle에 맞춰 정리되어야 하는 리스너나 작업이

kotlinworld.com

 

 


참고자료 : 

https://developer.android.com/jetpack/compose/side-effects?hl=ko 

 

Compose의 부수 효과  |  Jetpack Compose  |  Android Developers

Compose의 부수 효과 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 부수 효과는 구성 가능한 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항입니다.

developer.android.com

 

반응형