지난 포스팅
https://jinudmjournal.tistory.com/148
지난포스팅에 이어서 Compose의 SideEffects에 대하여 학습하는 시간을 가지게 되었습니다.
이번 시간에는 Compose 상태를 Non - Compose 상태로 변환하는 방법과 반대의 경우를 살펴보겠습니다.
Non - Compose -> Conpose
Compose에서는 STATE<T>를 통해 상태를 관찰하고 Composable을 업데이트 합니다.
따라서 Flow, LiveData, Rxjava 등의 관찰가능한 타입들을 Compose에서 사용하려면 State로 변환하는 과정이 필요하며,
이를 위해서 productState를 활용할 수 있습니다.
produceState는 관찰하고 있는 값이 변경되면 State<T>를 반환합니다.
Compose에서는 Flow, LiveData를 위해 State로 변환하는 함수를 지원합니다.
produceState가 컴포지션을 시작하면 프로듀서가 실행되고 컴포지션을 종료하면 취소합니다.반환 된 State는 동일한 값을 설정해도 리컴포지션이 트리거되지 않습니다.
produceState가 코루틴을 만드는 경우에도 정지되지 않는 데이터 소스를 관찰하는 데 사용할 수 있습니다.
이 소스의 구독을 삭제하는 경우 awaitDispose 함수를 사용할 수 있는데요, 아래 문서를 참고할 수 있습니다.
아래 예는 produceState를 사용하여 네트워크에서 이미지를 로드하는 방법을 사용하였습니다.
loadNetworkImage는 Composable 함수로 다른 컴포저블에서 사용할 수 있는 State를 반환합니다.
Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<Image>> {
// Result.Loading을 초기값으로 사용하여 State<T>를 생성합니다.
// `url` 또는 `imageRepository`가 변경되면 실행 중인 생산자는
// 취소되고 새 입력으로 다시 시작됩니다.
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
// 코루틴에서는 정지 호출을 할 수 있습니다.
val image = imageRepository.load(url)
// 오류 또는 성공 결과로 상태를 업데이트합니다.
// 이 상태를 읽는 재구성이 트리거됩니다.
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
주의할 점은 반환 유형이 있는 Composable 함수는 이름을 소문자로 시작하여 지정해야 합니다. (loadNetworkImage)
Compose State -> Non - Compose State
Compose 상태를 Compose에서 관리하지 않은 객체와 공유하려면 리컴포지션 성공 시마다 호출되는 SiedEffect를 활용합니다.
이전에 학습한 LaunchedEffect와 비슷한 방식으로 동작하는데요, SideEffect 컴포저블을 활용합니다.
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// 구성이 성공할 때마다 FirebaseAnalytics를 다음으로 업데이트합니다.
// 현재 사용자의 userType을 가져와 향후 분석을 보장합니다.
// 이벤트에는 이 메타데이터가 첨부되어 있습니다.
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
SideEffect는 쉽게 사용될 수 있지만, key 값에 따라 제어하거나 Composable이 Dispose되는 경우에는 사용하기 어렵습니다.
따라서 LaunchedEffect나 DisposableEffect를 활용하는 것을 권장한다고 합니다.
derivedStateOf
derivedStateOf는 하나 이상의 상태 객체를 다른 상태로 변환할 때 사용합니다.스크롤이 경계값을 통과하는지, 리스트의 항목이 임계값보다 큰지 등의 유효성 검사에 활용합니다.composable 함수 안에서 사용해야 하며, recomposition이 일어나도 살아남아야 합니다. 따라서 remember을 활용합니다.
val result = remember(state1, state2) { calculation(state1, state2) }
val result1 = remember { derivedStateOf { calculation(state1, state2) } }
위와 같이 사용할 경우, remember에 key를 지정하는 방식은
key가 변경되는 만큼 업데이트가 되어야 하는 경우에 사용하며, key마다 다른 값의 remember를 반환합니다.
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>() }
// todoTasks 또는 highPriorityKeywords가 있는 경우에만 우선순위가 높은 작업을 계산합니다.
// 재구성할 때마다 변경하는 것이 아니라 변경합니다.
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
}
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(highPriorityTasks) { /* ... */ }
items(todoTasks) { /* ... */ }
}
/* 사용자가 목록에 요소를 추가할 수 있는 나머지 UI */
}
}
위 코드에서의 동작 방식은 아래와 같습니다.
- derivedStateOf가 todoTasks가 변경될 때마다, highPriorityTasks에 대한 계산을 실행
- 그에 따라 UI를 업데이트 합니다.
- highPriorityKeywords가 변경되면 remember 블록이 실행됩니다.
- 이전 파생 상태 객체 대신 새로운 객체를 생성하고 기억합니다.
결론적으로 highPriorityTasks를 계산하기 위한 필터링이 비용이 많이 들 수 있으므로,
매 리컴포지션 시가 아닌 목록이 변경될 때만 실행하도록 동작할 수 있습니다.
snapshotFlow
Compose의 상태를 Flow로 변환합니다.
snapshotFlow를 활용하여 State<T> 객체를 Cold Flow로 변환합니다.
snapshotFlow 블록 내에서 읽은 State 객체 하나가 변경되었을 때,
새 값이 이전에 내보낸 값과 같지 않은 경우 Flow에서 새 값을 수집기에 내보냅니다.
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
위 코드에서 listState.firstVisibleItemIndex는 Flow 연산자의 이점을 활용할 수 있는 Flow로 변환 됩니다.
리뷰
Compose에서 자주 사용하는 효과에 대해 알아보았습니다.
개발을 진행하면서 급하게 compose를 학습하였기 때문에 어떤 의미를 가지고 사용되어지는지 모른채 무작정 사용하게 되었는데요.
이번 리뷰를 통해서 제대로 학습할 수 있었던 것 같습니다.