Compose의 SideEffects (1)

 

 

 Composable에서의 non-composable

SideEffects란 Composable 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항입니다.

Composable의 수명 주기 및 속성으로 인해 SideEffects를 최소화하는 것이 좋습니다.

 

하지만 스백바를 표시하거나 특정 상태 조건에 따라 다른 화면으로

이동하는 등의 일회성 이벤트를 처리할 때 SideEffects가 필요하게 됩니다.

 

 

LaunchedEffect

LaunchedEffect는 컴포저블 범위에서 정지 함수를 실행할 때 사용합니다.

매개변수로 전달 된 코드 블록으로 코루틴이 실행되는데요.

LaunchedEffect가 컴포지션을 종료하면 코루틴이 취소됩니다.

 

    LaunchedEffect(Unit) {
        detailViewModel.initAppState(appState,diaryState)
        detailViewModel.setDiaryDetail()
    }

 

composition 진입 및 key가 업데이트될 때 실행되며, key가 변경될 때마다 다시 람다를 실행합니다.

위와 같이 Key를 Unit으로 지정할 경우 LaunchedEffect 내의 블록은 싱글턴으로 한 번만 실행되게 됩니다.

when을 활용한 여러 개의 key의 변화에 대한 대응도 지정할 수 있습니다.

 

 

rememberCoroutineScope

컴포지션 인식 범위를 확보하여, 컴포저블 외부에서 코루틴을 실행하는 함수입니다.

LaunchedEffect의 경우 Composable 함수 내에서만 사용할 수 있습니다.

반면에 rememberCoroutineScope를 활용한 경우 컴포지션을 종료한 후 자동으로 취소되도록 외부에 선언할 수 있습니다.

 

https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#rememberCoroutineScope(kotlin.Function0)

 

androidx.compose.runtime  |  Android Developers

androidx.compose.desktop.ui.tooling.preview

developer.android.com

 

코루틴 하나 이상의 수명 주기를 수동으로 관리하는 경우에 사용합니다.

예로 사용자 이벤트가 발생할 때 애니메이션 등을 취소하는 상황이 있을 수 있습니다.

 

        rememberCoroutineScope().launch {
            delay(500)
            naviController.navigate("ssoScreen")
        }

 

위와 같이 딜레이 후 다음 화면으로 이동하는 코드를 작성하였습니다.

만약에 다른 사용자 이벤트가 발생하거나, 특정 상황으로 취소되는 경우 위 구문이 자동으로 취소되도록 동작하게 됩니다.

 

주의할 점은 non-composable에서 코루틴을 시작하면, 정의되지 않은 순간에 새 코루틴을 시작할 위험이 있습니다.

rememberUpdatedState

주요 매개변수 중 하나가 변경되면 LaunchedEffect가 다시 시작되게 되는데요,

하지만 경우에 따라 효과에서 값이 변경되면 효과를 다시 시작하지 않을 값을 저장해 둘 수 있습니다.

이러한 기능을 위해서 rememberUpdatedState를 사용하여 캡처하고 업데이트할 수 있는 값의 참조를 만들어야 합니다.

 

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

 

값이 들어올 때마다 새로운 값을 remember 된 mutableState에 직접 적용시켜 줍니다.

Composable의 데이터가 바뀔 때 하위 Composable의 remember된 데이터를 바꾸어야 할 경우 사용합니다.

LaunchedEffect 내부의 값을 변경해야하지만 새로운 coroutine을 실행하지 않아야 하거나

effect가 오랜시간 걸릴 때 작업을 유지해야 할 경우rememberUpdatedState를 사용합니다.

 

@Composable
fun ShowSnackBar(scaffoldState: ScaffoldState = rememberScaffoldState(), timeoutText: String) {
    val currentTimeoutText by rememberUpdatedState(timeoutText)

    LaunchedEffect(true) {
        try {
            delay(2000L)
            scaffoldState.snackbarHostState.showSnackbar(currentTimeoutText)
        } catch (ce: CancellationException) {

        }
    }

}

 

위의 경우 timeText가 변하더라도, LaunchedEffect를 재시작하지 않고, 내부 구문을 실행합니다.

만약 rememberUpdatedState를 사용하지 않는다면, timeoutText가 변해도 똑같은 showSnackbar 구문을 표시합니다.

 

DisposableEffect

LaunchedEffect와 동작이 동일하지만, 정리가 필요한 효과일 때 사용합니다.

키가 변경되거나 Composable이 컴포지션을 종료한 후 정리해야 하는 부수효과에 사용하며, 

DisposableEffect 키가 변경되면 컴포저블이 현재 효과를 삭제하고 다시 호출하여 재설정해야 합니다.

 

https://developer.android.com/jetpack/compose/side-effects?hl=ko#state-effect-use-cases

 

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

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

developer.android.com

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit,
    onStop: () -> Unit
) {
    // 새로운 람다식이 들어왔을 때 안전하게 실행할 수 있도록 rememberUpdatedState를 사용
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // lifecycleOwner가 변경되면 DisposableEffect내에 있는 코루틴이 다시 시작
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        // Effect가 Composition을 떠날 때 observer를 삭제
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}

 

onDispose 절을 코드 블록의 맨 끝에 적어둬야 하며, 그렇지 않을 시 오류가 발생합니다.

lifecycleOwner가 변경되면, DisposableEffect 내의 코루틴이 다시 시작됩니다.