Compose Side Effect

컴포즈에서 UI 의 상태를 변경하는 작업

Compose Side Effect

Side Effect


안드로이드 컴포즈에서 effect 란 용어는 주로 Side Effect 를 말하는데, 안드로이드 컴포즈에서 UI 의 상태를 변경하는 작업을 의미한다.


LaunchedEffect


컴포저블의 범위에서 중단 함수(suspend function) 실행을 할 때 사용한다.

LaunchedEffect 는 하나의 키를 받아야 하며, 키가 바뀌면 새로운 코루틴 스코프를 만들어서 실행하며, 만약 LaunchedEffect 가 컴포지션 범위에서 빠지게 되면 코루틴이 캔슬된다.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {
    if (state.hasError) {
        LaunchedEffect(scaffoldState.snackbarHostState) {
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error Message",
                actionLabel = "Retry Message"
            )
        }
    }

    Scaffold(scaffoldState = scaffoldState)
}



rememberCoroutineScope


코루틴 스코프를 만들고 유지하기 위하여 사용하는 것으로, 컴포지션 인식 범위를 확보해서 컴포저블 외부에서 코루틴을 실행할 수 있도록 해준다.

@Composable
fun MoviesScreen(
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {
    val scope = rememberCoroutineScope()
    Scaffold(scaffoldState = scaffoldState) {
        Column {
            Button(
                onClick = {
                    scope.launch {
                        scaffoldState.snackbarHostState.showSnackbar("Something Happened!")
                    }
                }
            ) {
                Text("Press Me")
            }
        }
    }
}
  • MoviesScreen 안에서 코루틴 스코프가 만들어진다.


rememberUpdatedState


값이 변경되는 경우에 그것을 이용하는 효과가 다시 시작되지 않게 되어야 하는 경우에 사용한다.

@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply {
    value = newValue
}
  • 값을 상태로 만들고 remember 한 후에 다시 apply 로 값을 대입한다.
  • remember 상태에서는 상태가 만들어지면 리컴포지션 과정에 예전 값을 가지게 되기 때문에 값을 다시 apply 로 설정하는 것이다.

@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }
}
  • LaunchedEffect 에 키에 바로 onTimeout 을 설정하지 않은 이유는 리컴포지션이 되면 delay 가 계속 호출되기 때문에, true 를 설정하여 리컴포지션에서 제외하여 한번만 호출되게 한 것이다.


DisposableEffect


“파일을 열었다가 닫는다” 와 같은 정리가 필요한 효과를 onDispose() 에서 관리할 수 있도록 해준다.

DisposableEffect 의 키가 바뀌면 dispose 하고 이펙트를 재시작하며, 컴포지션에서 이펙트가 나가도 onDispose 를 호출한다.

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit,
    onStop: () -> Unit
) {
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    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)

    onDispose {
        lifecycleOwner.lifecycle.removeObserver(observer)
    }
}



SideEffect


컴포즈 상태를 비컴포즈 상태로 바꿔주며, 리컴포지션이 발생할 때 마다 호출된다.

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}



produceState


비컴포즈 상태를 컴포즈 상태로 바꿔주며, 코루틴을 제공한다.

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {
    return produceState<Result<Image>>(
        initialValue = Result.Loading,
        url,
        imageRepository
    ) {
        val image = imageRepository.load(url) // suspend call
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}
  • State 를 반환하는데(State<Result<Image>>), value 값을 통해 상태를 반환한다.


derivedStateOf


하나 이상의 상태 객체를 다른 상태 객체로 변환한다.

derivedStateOf 는 상태의 값이 바뀌지 않으면 값이 유지되는 특징이 있으며 값이 바뀔 때만 Statevalue 가 바뀌기 때문에 불필요한 리컴포지션을 방지하는 장점이 있다.

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

    val highPriortyTasks by remember(highPriorityKeywords) {
        derivedStateof {
            todoTasks.filter {
                it.containWord(highPriorityKeywords)
            }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
    }
}



snapshotFlow


컴포즈의 상태를 Flow 로 변환한다.

State<T> 를 콜드 Flow 로 변경하며, 이전에 방출한 값(emitted value)과 다를 경우에 방출(emit)한다.

val listState = rememberLazyListState()

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

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