Compose에는 다양한 부수효과들이 있다. 천천히 살펴보자
코루틴 스코프를 composable안에 열어서 suspend fun을 돌아갈 수 있게 해준다.
대표적으로 snackbar를 컴포즈 안에서 띄우려 할 때 쓰일 수 있다.
@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) {
}
}
LaunchedEffect가 인자로 받는 Key
, 위의 경우에는 scaffoldState.snackbarHostState
가 값이 바뀔 때, 코루틴을 다시 만들어 실행한다.
LaunchedEffect가 composition에서 빠지면 coroutine도 같이 제거된다.
rememberCoroutineScope는 Composable의 생명주기에 맞게 compose가 동작되도록 한다.
즉 화면이 전환되어 composable이 destroy되면서 이 안에 있는 돌아가는 coroutine도 종료되는 것이다.
값이 변경되는 경우 다시 시작되지 않는 효과에서 값 참조를 하는 기능이다.
value가 바뀌었을 때 state도 바뀌도록 하는 기능
쉽게 말해 rememberUpdatedState() 의 매개변수 데이터의 값이 바뀌는 경우, 이에 해당하는 state를 업데이트 해주어, recomposition의 대상이 안되는 것을 recomposition이 되도록 해준다.
예를 들어 이런 코드가 있다고 하자.
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState (onTimeout)
LaunchedEffect (true) {
delay (SplashWaitTimeMillis)
currentOnTimeout ()
}
}
어떠한 다른 composable에서 LandingScreen을 부르면서 인자로 onTimeOut을 준다고 하면, state의 value를 가지고 있는 currentOnTimeOut이 덥데이트 된다. 여기서 launchdEffect 안을 true로 하는 이유는 딱 한번만 변화를 감지해서 currentOnTimeOut을 부르려고 하기 때문이다.
만약 이렇게 하지 않고 true 대신 currentOnTimeout 자체를 인자로 받는다면 계속 함수를 호출해서 무한반복되는 코드가 될 수 있다.
정리가 필요한 효과이다. 즉, observer등 정리가 필요한 이벤트 등을 넣는 effect이다.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
_onStartLogging: () - Unit,
_onStopLogging: ( - Unit
)
val startLoggingOnStart by rememberUpdatedState(_onStartLogging)
val stopLogging0nStop by rememberUpdatedState(_onStopLogging)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver {
_, event-
if (event = Lifecycle.Event.ON_START) {
startLoggingOnStart()
} else if (event = Lifecycle.Event.ON_STOP) {
stopLoggingOnStop()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
println("Kotlinworld » Observer Attached")
onDispose {
// Composable0l dispose UH observer
lifecycleOwner.lifecycle.removeObserver(observer)
printin("Kotlinworld » Observer Removed")
}
}
DisposableEffect를 쓰면 lifecycleOwner가 종료되면 onDispose가 불리면 observer가 제거되게 할 수 있다.
한가지 궁금한 점은 DispoableEffect에서 lifecycleowner를 가져와서 쓴다.
일반적으로 oncreate, onpause 등 라이프사이클에 대한 분기 처리는 override 된 함수에서 사용했는데, 왜 이렇게 하는지는 다소 궁금하다.
컴포즈는 뷰 단에서 모든 것을 관리하는 구조로 하는 것 같다.
Compose에서 관리되는 객체가 아닌 다른 객체에 compose의 상태를 공유하기 위한 용도로, Composition이 성공적으로 완료되면 진행할 동작을 예약할 때 SideEffect를 사용.
@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher,
isEnable: Boolean = false,
onBack: () -> Unit) {
..
// Remember in Composition a back callback that calls the `onBack` lambda
val backCallback = remember {
..
}
SideEffect {
backCallback.isEnabled = isEnable
}
// If `backDispatcher` changes, dispose and reset the effect
DisposableEffect(backDispatcher) {
..
}
}
비 composable 값들을 composable state로 만들어준다.
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<Image>> {
// 초기값으로 Result.Loading을 세팅하고, url, imageRepository가 변경되면
// 실행중인 producer(coroutine)이 취소되고 재시작됨
return produceState(initialValue = Result.Loading, url, imageRepository) {
// coroutine scope 내부이므로 suspend function을 호출할 수 있습니다.
val image = imageRepository.load(url)
// 진행 결과에 따라 value에 값을 세팅하여 emit 합니다.
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
produceState 자체에서 코루틴 스코프를 생성하기 때문에 suspend fun을 부를 수 있다.
그리고 result로 image를 return 한다.
하나 이상의 상태 객체를 다른 상태로 변환한다.
@Composable
fun TodoList(
highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
val todoTasks = remember { mutableStateListOf<String>() }
// high priority keyword의 계산은 todoTasks 또는 highPriorityKeywords 변경시에만 수행
// recomposition 되더라도 해당 param의 변경이 없다면 수행되지 않음.
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf {
todoTasks.filter { it.containsWord(highPriorityKeywords) }
}
}
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(highPriorityTasks) { /* ... */ }
items(todoTasks) { /* ... */ }
}
/* Rest of the UI where users can add elements to the list */
}
}
derivedStateOf는 상태를 만들어준다.
derivedStateOf는 안의 계산된 값이 바뀔 때에만 state의 value를 바꿔준다.
State를 Flow로 바꿔준다.
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
snapshotFlow 역시 값이 바뀌었을 때에만 실행이된다.
참고로 viewmodel에서 선언된 flow를 state로 바꿔주는
Flow.collectAsState()
도 있다. 통신에 활용하자.