본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android] Compose Side-effects

by 돼지왕 왕돼지 2023. 8. 16.
반응형

https://developer.android.com/jetpack/compose/side-effects

#
LaunchedEffect : composable 안에서 suspend function 실행

composition 에 들어가면 LaunchedEffect 의 block 코드가 새로운 coroutine 에서 실행됨.
composition 에서 벗어나면 LaunchedEffect 의 block 코드는 cancel 됨.
Recomposition 에서는 LaunchedEffect 의 key 가 변경되는 경우 기존 coroutine cancel 하고 relaunch 됨.

if (state.hasError) {
    // `LaunchedEffect` will cancel and re-launch if `scaffoldState.snackbarHostState` changes
    LaunchedEffect(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.
        snackbarHostState.showSnackbar(
            message = "Error message",
            actionLabel = "Retry message"
        )
    }
}

 

#
rememberCoroutineScope : composable 바깥에서 composition-aware scope 를 얻음

composable 바깥에서 coroutine 실행하지만, composition 에서 벗어날 때 cancel 하고 싶은 경우 사용함.
rememberCoroutineScope 는 CoroutineScope 를 return 하며, 이는 호출장소의 composition 에 bound 됨.
이 scope 은 bound 된 composition 에서 벗어나면 cancel 된다.

@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {
    // Creates a CoroutineScope bound to the MoviesScreen 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")
            }
        }
    }
}

 

#
rememberUpdatedState : 변경되었을 때 effect 를 restart 시키지 않으면서 effect 에서 사용되는 value 참조

Effect 내부에서 참조되는 value 인데, effect 를 새로 launch 시키고 싶지 않을 때 사용하는데,
보통 장기간 살아있어야 하는 작업이나, 재시도하기에 비싼 effect 등에 활용된다.

@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 should not start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    ...
}

LaunchedEffect(true) 같은 코드는 위험하므로 조심해서 써야 함.

 

#
DisposableEffect: cleanup 이 필요한 effect

key 가 변경되거나, composition 에서 나갈 때 clean up 이 필요할 때 이 녀석을 쓰면 됨.
이 녀석의 마지막에는 onDispose 구문을 써줘야 하며, 여기를 비워두는 건 좋지 않음.

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)
    }
}

 

#
SideEffect : Compose state 를 non-compose code 에 발행

recomposition 마다 코드가 실행됨.

 

#
produceState : Non compose state 를 compose state 로 전환

Flow, LiveData, RxJava 등을 composition 에서 사용하는 State 로 전환하는 작업을 함.
producer 의 코드는 produceState 가 composition 에 진입하면 수행되고, composition 에서 나가면 cancel 됨.
return 되는 State 는 conflates 로 이는 StateFlow 처럼 동일 value 발행은 recomposition 을 야기하지 않음.

produceState 는 coroutine 을 만들지만, non-suspending source 를 observe 하는데 사용될 수 있음.
이 경우 awaitDispose 를 사용하여 observe 를 해지시킬 수 있음.

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

 

#
derivedStateOf: 1개 이상의 state 를 모아서 다른 state 로

다른 state 를 활용하여 계산하거나 유도된 state 를 말함.
이 계산이나 유도 과정에서 사용된 state 중 하나라도 변경되면 다시 계산 또는 유도 로직이 돌게 됨.
derivedStateOf 는 에 대한 update 자체가 recompose 를 야기하진 않음. 사용처가 있어야 함.

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or highPriorityKeywords change, not on every recomposition
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter { task ->
                highPriorityKeywords.any { keyword ->
                    task.contains(keyword)
                }
            }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

 

#
snapshotFlow: Compose state 를 Flow 로 전환

State 를 cold Flow 로 변경함.
snapshotFlow 의 block 은 collect 가 발생할 때 돌게 됨. 이 block 안에서 참조하는 State 값이 변경될 때, flow 는 collector 에 새로운 값을 emit 하며, 동일값은 발행하지 않음.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

 

#
Restarting effects

LaunchedEffect, produceState, DisposableEffect 과 같은 compose 의 effect 들은 실행중인 effect 를 취소하고 새로운 key 를 기반으로 재시작하기 위해 몇개의 key 를 받을 수 있음.
기대한 것보다 적은 effect restarting 은 버그를 생산하기 쉽고, 기대한 것보다 많은 effect restarting 은 비효율적임.
variable 이 restarting 을 야기하면 안 된다면, 해당 variable 은 rememberUpdatedState 로 감싸져야 함.
key 없는 remember 에 감싸져서 variable 이 절대 변하지 않는다면, 해당 variable 도 effect 에 넘기면 안 됨.

 

 

반응형

댓글