시배's Android

Compose docs | Side-effects in Compose 본문

Android/Compose Docs

Compose docs | Side-effects in Compose

si8ae 2023. 8. 8. 00:32

State and effect use cases

LaunchedEffect L run suspend functions in the scope of a composable

LaunchedEffect 구성에 진입하면 코드 블록과 함께 코루틴을 실행합니다. LaunchedEffect 구성을 떠나면 해당 코루틴은 취소됩니다. LaunchedEffect 다른 키와 함께 재구성되는 경우, 기존의 코루틴은 취소되고 새로운 suspend 함수가 새로운 코루틴에서 실행됩니다.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    snackbarHostState: SnackbarHostState
) {

    // If the UI state contains an error, show snackbar
    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"
            )
        }
    }

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding ->
        // ...
    }
}

위의 코드에서 상태(state) 오류가 포함되어 있으면 코루틴이 트리거되고, 상태에 오류가 없으면 코루틴이 취소됩니다. LaunchedEffect 호출 위치가 if 내부에 있으므로, 문이 false 경우 LaunchedEffect 구성에서 제거되며 따라서 코루틴이 취소됩니다.

rememberCoroutineScope : obtain a composition-aware scope to launch a coroutine outside a composable

LaunchedEffect는 구성 요소 함수로, 다른 구성 요소 함수 내에서만 사용할 수 있습니다. 구성 요소 밖에서 코루틴을 시작하되, 구성을 벗어날 때 자동으로 취소되도록 범위를 지정하려면 rememberCoroutineScope를 사용하세요. 또한 하나 이상의 코루틴 수명 주기를 수동으로 제어해야 할 때도 rememberCoroutineScope를 사용하십시오. 예를 들어 사용자 이벤트 발생 시 애니메이션을 취소하는 경우 등입니다.

 

rememberCoroutineScope 구성이 호출된 지점에 바인딩된 CoroutineScope 반환하는 구성 요소 함수입니다. 범위는 호출이 구성을 벗어날 취소됩니다.

rememberUpdateState : reference a value in an effect that shouldn't restart if the value changes

LaunchedEffect는 키 매개변수 중 하나가 변경되면 재시작됩니다. 그러나 어떤 상황에서는 효과를 재시작하지 않기를 원하는 값을 효과에 포착하고자 할 수 있습니다. 이를 위해 rememberUpdatedState를 사용하여 해당 값을 참조하고 포착하고 업데이트할 수 있습니다. 이 접근 방식은 재생성하고 재시작하기 어렵거나 불가능한 오래 지속되는 작업이 포함된 효과에 유용합니다.

 

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 */
}

효과(Effect) 호출 지점의 라이프사이클과 일치하도록 하려면, Unit 또는 true 같이 항상 변경되지 않는 상수를 매개변수로 전달합니다. 위의 코드에서는 LaunchedEffect(true) 사용합니다. LandingScreen 재구성될 항상 최신 값을 포함하는 onTimeout 람다를 보장하기 위해 onTimeout rememberUpdatedState 함수로 래핑되어야 합니다. 코드에서 반환된 State currentOnTimeout 효과 내에서 사용되어야 합니다.

DisposableEffect : effects that require cleanup

키가 변경될 또는 구성 요소가 구성을 떠날 정리(clean up)되어야 하는 부작용(side effect)에는 DisposableEffect 사용하세요. DisposableEffect 키가 변경되면, 구성 요소는 현재 효과를 해제(정리)하고, 다시 효과를 호출하여 재설정해야 합니다.

@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 */
}

SideEffect : publish Compose state to non-compose code

Compose 상태를 Compose에서 관리되지 않는 객체와 공유하려면, SideEffect(composable) 사용하세요. SideEffect 성공적인 재구성마다 호출되므로 Compose에서 관리되지 않는 객체와 상태를 공유하는 데에 사용될 있습니다.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

produceState : convert non-Compose state into Compose state

produceState는 Composition 범위 내에서 실행되는 코루틴을 시작하여 값을 반환하는 State에 값을 전달할 수 있게 해주는 함수입니다. 이를 사용하여 비-Compose 상태를 Compose 상태로 변환할 수 있습니다. 예를 들어 Flow, LiveData, 또는 RxJava와 같은 외부 구독 기반 상태를 Composition으로 가져올 수 있습니다.

 

produceState가 Composition에 들어갈 때 프로듀서(producer)가 시작되며, Composition을 벗어날 때 취소됩니다. 반환된 State는 이전 값과 현재 값을 합쳐놓았으며, 동일한 값을 설정해도 재구성이 트리거되지 않습니다.

 

produceState는 코루틴을 생성하지만, 비-중단적인 데이터 소스를 관찰하는 데에도 사용할 수 있습니다. 해당 소스에 대한 구독을 해제하려면 awaitDispose 함수를 사용합니다.

@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 : convert one or multiple state objects into another state

derivedStateOf 함수는 특정 상태가 다른 상태 객체에서 계산되거나 유도될 사용됩니다. 함수를 사용하면 계산이 필요한 경우에만 수행되며, 계산에 사용된 상태 하나가 변경될 때마다 다시 계산되도록 보장합니다.

@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 : convert Compose's State into Flows

snapshotFlow 함수는 State<T> 객체를 cold Flow로 변환하는 데에 사용됩니다. snapshotFlow는 블록을 수집할 때 실행되며, 그 안에서 읽은 State 객체들의 결과를 발행합니다. snapshotFlow 블록 내에서 읽은 State 객체 중 하나가 변경되면, 새 값이 이전에 발행한 값과 같지 않은 경우 Flow는 해당 새 값을 수집자(collector)에게 발행합니다. (이 동작은 Flow.distinctUntilChanged와 유사합니다.)

 

, snapshotFlow 사용하여 State 객체의 변경을 감지하고 해당 변경이 발생할 때마다 Flow 값을 보낼 있습니다. 이는 상태가 변경될 Compose Composition 내에서 동작을 트리거하는 데에 유용하게 사용할 있습니다.

val listState = rememberLazyListState()

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

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