[Compose Study] rememberCoroutineScope

728x90
반응형

* 기본적으로 Composable 함수 안에서는 기존의 방식으로 코루틴을 사용할 수 없다. 

  대신 Compose에서도 코루틴을 구현할 수 있도록 Effect API라는 것을 제공한다.

* 안드로이드에서는 Compose 함수 외부에서 앱 상태가 변화하는 것을 부수 효과(Side Effect)라고 한다.

  그리고 공식문서에는 이러한 부수 효과를 일으키는 요소가 Compose 함수 내에 존재해서는 안된다고 정의하고 있다.

컴포저블의 수명 주기 및 속성(예: 예측할 수 없는 리컴포지션 또는 다른 순서로 컴포저블의 리컴포지션 실행, 삭제할 수 있는 리컴포지션)으로 인해 컴포저블에는 부수 효과가 없는 것이 좋습니다.

 

* Compose 구성요소들은 언제 리컴포지션이 발생할 지 모르기 때문에 Compose 함수에서 부수 효과를 발생시키는 경우

  예상치 못한 현상이 발생할 수도 있다는 뜻이다. 하지만 부수 효과가 필요할 때도 있기 때문에 이를 위해서 나온 것이

  바로 부수 효과(Side Effect) API이다.

 

LaunchedEffect 

* 부수효과 API의 가장 대표적인 컴포지션이 바로 LaunchedEffect이다. LaunchedEffect를 사용하면 컴포저블 함수

  내에서도 안전하게 정지 함수를 호출할 수 있다.

* LaunchedEffect는 state 형태의 key 값과 실행 블럭을 매개변수로 받는다. 

  key 값에 변화가 생기면 실행 블럭 내부에 있는 코드가 실행된다.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {

    // If the UI state contains an error, show snackbar
    if (state.hasError) {

        LaunchedEffect(scaffoldState.snackbarHostState) {
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(scaffoldState = scaffoldState) {
        /* ... */
    }
}

* 위의 LaunchedEffect 의 경우 scaffoldState.snakbarHostState를 key 값으로 받고 있으며,

  해당 값에 변화가 생길 경우 LaunchedEffect 실행 블럭 내부의 코드가 실행되어 SnackBar를 보여준다.

rememberCoroutineScope

* 부수효과를 구현하는 또 다른 방법으로는 rememberCoroutineScope 함수로 호출된 컴포지션에 바인딩된 

  CoroutineScope를 반환하여 해당 scope 안에서 suspend 함수를 구현하는 방법이다.

* rememberCoroutineScope는 호출된 컴포지션에 바인딩되어있는 scope를 반환하기 때문에 해당 컴포지션이

  취소되면 coroutineScope도 같이 취소된다.

 

@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            /* ... */
            Button(
                onClick = {
                    // Create a new coroutine in the event handler to show a snackbar
                    scope.launch {
                        scaffoldState.snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

* val scope = rememberCoroutineScope()를 통해서 생성된 scope는 MovieScreen 이라는 컴포저블 함수의

  생명주기를 따르게 된다.

* 이때 주의해야할 점은 해당 scope로 생성된 코루틴은 어디까지나 MovieScreen이라는 컴포저블 함수의

  생명주기만을 따르는 거지 실제 코루틴의 위치는 컴포저블 함수 외부에 있다는 사실이다.

* 처음에 설명했듯이 컴포저블 함수 내부에서는 외부의 값을 바꾸는 부수효과를 줄 수 없다. 대신 부수효과를 주기 위한

  코루틴을 외부에 생성하고 이 코루틴의 생명주기가 Composable 함수 생명주기를 따르게 만들 뿐이다.

 

Activity의 lifecycle을 따르는 Coroutine을 Composable에서 생성할 때의 문제점

* FirstScreen

@Composable
fun FirstScreen(
    snackbarHostState: SnackbarHostState,
    onScreenChange: () -> Unit,
    coroutineScope: CoroutineScope = rememberCoroutineScope() // 인자가 안넘어오면 rememberCoroutineScope 사용
) {
    Column() {
        Button(
            onClick = {
                coroutineScope.launch {
                    snackbarHostState.showSnackbar("Show Snackbar!")
                }
            }
        ) {
            Text("Show Snackbar")
        }
        Button(
            onClick = {
                onScreenChange()
            }
        ) {
            Text("Navigate to another Screen")
        }
    }
}

 

* show snackBar 버튼을 누르면 아래에 SnackBar가 몇 초동안 표시되고, 두번째 Navigate 버튼을 누르면

 두번째 화면으로 넘어가는 간단한 코드이다.

 

* MainActivity

class MainActivity : ComponentActivity() {
    @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val snackbarHostState = remember { SnackbarHostState() }
            var isFirstScreen by remember { mutableStateOf(true) }

            Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
                if (isFirstScreen) {
                    FirstScreen(
                        snackbarHostState = snackbarHostState,
                        onScreenChange = {
                            isFirstScreen = false
                        },
                        coroutineScope = lifecycleScope
                    )
                } else {
                    SecondScreen()
                }
            }
        }
    }
}

 

* 16번째줄 coroutineScope = lifecycleScope를 넣고 FirstScreen에 매개변수로 넘기면, 생명주기가 Activity의

   생명주기를 따르다보니, 첫번 째 Screen Composable이 파괴 될때도 위의 coroutine Job이 유지된다.

 

if (isFirstScreen) {
                    KotlinWorldScreen(
                        scaffoldState = scaffoldState,
                        onScreenChange = {
                            isFirstScreen = false
                        }
                        // CoroutineScope 넘기지 않음
                    )

 

* 그럼 FirstScreen에 CoroutineScope를 넘기지 않으면 어떻게 될까? 우선 FirstScreen코드에서 매개변수로

  coroutineScope가 비어있으면 coroutineScope = rememberCoroutineScope가 들어가도록 설정했다.

* rememberCoroutineScope은 FirstScreen Composable의 생명주기를 따르다 보니, FirstScreen이

   파괴될 때 rememberCoroutineScope에서 실행되는 코루틴 또한 취소된다. 따라서 SecondScreen으로

  넘어갔을 때 Snackbar가 사라지는 것을 확인할 수 있다.

 

" Composable이 파괴될 때 파괴되는 코루틴을 생성 해야될 때는 rememberCoroutineScope을 사용하도록 하자 "

 

rememberCoroutineScope 사용시 주의 사항

* rememberCoroutineScope는 컴포저블 외부에 있지만 컴포지션을 종료한 후 자동으로 취소되도록 범위가 지정된

  코루틴을 실행하거나, 코루틴 하나 이상의 수명 주기를 수동으로 관리해야 할 때(예: 사용자 이벤트가 발생할 때

  애니메이션을 취소해야 하는 경우) 유용하게 사용할 수 있다.

* 반면에 외부에 새로 코루틴을 만든다는 특성 때문에 사용시 주의사항이 있다.

* 바로 재구현(Recomposition)이 일어날 때는 rememberCoroutineScope로 생성된 코루틴이 취소되지 않는다는 것이다.

* 예를 들어, 컴포저블 함수 안에 Button을 만들고 onClick 블럭에 rememberCoroutineScope로 코루틴을 생성하며

  이 작업으로 재구현이 발생한다고 가정해보자.

* 사용자가 버튼을 누를때마다 재구현된 Button에서 계속해서 새로운 코루틴이 생성될 것이다.

* 이처럼 재구현이 상당히 자주 일어나는 화면에서 rememberCoroutineScope를 설정했다면 지나치게 많은 코루틴이

  생성되어 심할 경우 앱 크래시를 유발할 수도 있다.

 

ScaffoldState Deprecated

* 찾아본 블로그에는 val scaffoldState를 사용하는 코드로 되어있었는데, 그대로 따라 치니 계속 찾을 수 없다고 에러가

  떠서 인터넷 검색을 해보니 material3부터는 scaffoldState 대신 snackbarHostState를 사용한다고 되어있었다. 

setContent {
            val scaffoldState: ScaffoldState = rememberScaffoldState()
            var isFirstScreen by remember { mutableStateOf(true) }
            Scaffold(
                scaffoldState = scaffoldState
            ) {
                if (isFirstScreen) {
                    KotlinWorldScreen(
                        scaffoldState = scaffoldState,
                        onScreenChange = {
                            isFirstScreen = false
                        },
                        coroutineScope = lifecycleScope // lifecycleScope 넘기기
                    )
                } else {
                    NewScreen()
                }
            }
        }

 

* 또한 아래와 같이 선언하는 것도 by, remember { }를 사용하는 것으로 코드를 수정했다.

val scaffoldState: ScaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }

 

----------------

참고한 블로그: https://kotlinworld.com/247

https://velog.io/@moonliam_/AndroidCompose-rememberCoroutineScope-vs-LaunchedEffect

 

 

728x90
반응형
TAGS.

Comments