Compose에서 사이드 이펙트는 컴포저블 함수의 범위를 벗어나는 모든 작업을 의미합니다.
이러한 작업들은 컴포저블의 UI 렌더링과 직접적인 관련이 없지만, 앱의 동작에 필수적인 요소들입니다.
Compose는 선언적 UI 프레임워크이며, 컴포저블은 여러 번 재실행될 수 있습니다. 따라서 사이드 이펙트를 적절히 관리하지 않으면 다음과 같은 문제가 발생할 수 있습니다
이러한 문제를 방지하기 위해 Compose는 다양한 사이드 이펙트 관리 도구를 제공합니다.
컴포저블의 수명 주기에 따라 코루틴을 실행하고 관리하는 컴포저블입니다.
LaunchedEffect가 컴포지션을 종료하면 코루틴이 취소됩니다. LaunchedEffect가 다른 키로 재구성되면 기존 코루틴이 취소되고 새 코루틴에서 새 정지 함수가 실행됩니다.
var pulseRateMs by remember { mutableStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
while (isActive) {
delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
alpha.animateTo(0f)
alpha.animateTo(1f)
}
}
컴포저블 외부에서 코루틴을 실행해야 할 때 사용합니다.
@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")
}
}
}
}
LaunchedEffect는 컴포저블 함수내에서만 사용할 수 있지만, 컴포저블 외부에 있지만 컴포지션을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 실행하려면 rememberCoroutineScope를 사용하면 됩니다.
Button을 onClick할 때 rememberCoroutineScope를 사용하여 Snackbar를 표시할 수 있습니다.
컴포저블이 재구성될 때 최신 값을 참조해야 하는 경우 사용합니다.
값이 변경되면 상태 객체가 업데이트되지만 중요한 점은 이 업데이트가 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 */
}
앱에 시간이 지나면 사라지는 LandingScreen이 있다고 가정해 보겠습니다. LandingScreen이 재구성되는 경우에도 일정 시간 동안 대기하고 시간이 경과되었음을 알리는 효과는 다시 시작해서는 안 됩니다.
리소스를 정리해야 하는 경우 사용합니다.
키가 변경되거나 컴포저블이 컴포지션을 종료한 후 정리해야 하는 경우
@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 */
}
DisposableEffect는 onDispose 절을 코드 블록의 최종 문장으로 포함해야 합니다. 그러지 않으면 IDE에 빌드 시간 오류가 표시됩니다.
Compose 상태를 Compose가 아닌 코드에 동기화해야 할 때 사용합니다.
SideEffect를 사용하면 리컴포지션이 성공할 때마다 효과가 실행됩니다.
즉, 리컴포지션이 성공적으로 보장되기 전에는 실행이 안된다는 뜻이다.
@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
}
예를 들어 애널리틱스 라이브러리를 사용하면 커스텀 메타데이터(이 예에서는 '사용자 속성')를 이후의 모든 애널리틱스 이벤트에 연결하여 사용자 인구를 분류할 수 있습니다. 현재 사용자의 사용자 유형을 애널리틱스 라이브러리에 전달하려면 SideEffect를 사용하여 값을 업데이트합니다.
외부 데이터 소스를 Compose 상태로 변환할 때 사용합니다.
Flow, LiveData 또는 RxJava와 같은 외부 구독 기반 상태를 컴포지션으로 변환하려면 이 코루틴을 사용하세요.
컴포지션을 시작하면 프로듀서가 실행되고 컴포지션을 종료하면 취소됩니다. 반환된 State는 합성됩니다. 동일한 값을 설정해도 리컴포지션이 트리거되지 않습니다.
@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)
}
}
}
다음 예에서는 produceState를 사용하여 네트워크에서 이미지를 로드하는 방법을 보여줍니다. loadNetworkImage 구성 가능한 함수는 다른 컴포저블에서 사용할 수 있는 State를 반환합니다.
다른 상태를 기반으로 새로운 상태를 계산할 때 사용합니다.
상태 객체 또는 입력이 UI가 실제로 업데이트해야 하는 것보다 더 자주 변경되어 불필요한 재구성이 발생할 수 있습니다.
컴포저블의 입력이 재구성해야 하는 것보다 더 자주 변경되는 경우 derivedStateOf 함수를 사용해야 합니다.
@Composable
fun MessageList(messages: List<Int>) {
Box(modifier = Modifier.fillMaxSize()) { // Box의 크기를 전체 화면으로 설정
val listState = rememberLazyListState()
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) { // LazyColumn도 전체 크기로 설정
items(messages) {
Text("Item $it", modifier = Modifier.height(20.dp))
}
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton(listState)
}
}
}
derivedStateOf를 사용하지 않으면 스크롤값이 스크롤할때마다 변경되서 리컴포지션이 발생하지만 derivedStateOf를 사용하면 값이 0보다 커진 경우만 동작을 해서 1~100사이로 스크롤를 계속해도 리컴포지션이 발생하지 않습니다.