Compose에서 Flow를 State로 변환할 때 collectAsState()를 사용한다.
Compose Extension 들의 구현을 알면 Compose에 대한 이해도도 키울 수 있을 것 같아 파악해보려 한다.
fun Flow.collectAsState(): State
@Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
collectAsState는 productState function을 호출합니다. 파라미터는 순서대로 초기값, Flow, Coroutine Context, 람다식을 전달하는데, 람다식은 Flow를 collect하고 emit 된 데이터는 value에 할당하는 간단한 동작을 합니다.
람다식은 suspend ProduceStateScope<T>.() -> Unit 타입이며, ProductStateScope<T>는 MutableState<T>를 상속받고 있기 때문에 value = it가 가능한 것입니다.
interface ProduceStateScope<T> : MutableState<T>, CoroutineScope {
suspend fun awaitDispose(onDispose: () -> Unit): Nothing
}
fun produceState(initialValue, key1, key2, producer): State
productState function을 들여다 보겠습니다.
@Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
간단하게 구현되어 있는데요. remember와 mutableStateOf 로 MutableState를 만들고 이를 리턴해줍니다.
그 중간에는 첫 Composition 시 실행되는 LaunchedEffect가 있습니다. 파라미터로 넘겼던 Flow와 CoroutineContext는 LaunchedEffect의 key 값으로 사용되고 있네요. LaunchedEffect 내에는 ProduceStateScopeImpl 생성 후 넘겨주었던 람다식을 호출하고 있습니다.
private class ProduceStateScopeImpl<T>(
state: MutableState<T>,
override val coroutineContext: CoroutineContext
) : ProduceStateScope<T>, MutableState<T> by state {
override suspend fun awaitDispose(onDispose: () -> Unit): Nothing {
try {
suspendCancellableCoroutine<Nothing> { }
} finally {
onDispose()
}
}
}
ProductStateScopeImpl 생성 시 remember 후 리턴해주었던 MutableState가 넘겨지는데, ProductStateScopeImpl은 받은 state를 delegation으로 MutableState를 구현해주고 있습니다. 이러면 collectAsState 람다식에서 value = it으로 할당한 것은 collectAsState와 productState에서 리턴된 MutableState에 할당한 것이라 볼 수 있습니다.
collectAsState 내에서만 동일한 기능을 직관적으로 구현하게 된다면 아래와 같이 작성할 수 있습니다.
@Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> {
val state = remember { mutableStateOf(initial) }
LaunchedEffect(this, context) {
if (context == EmptyCoroutineContext) {
collect { state.value = it }
} else withContext(context) {
collect { state.value = it }
}
}
return state
}
Flow를 collect 후 emit 때 마다 mutableState에 할당해주는 간단한 방법이네요.
Extension들을 보다보면 생각보다 간단한 구현으로 유용하고 강력한 기능을 제공하는 것 같습니다.
LaunchedEffect, remember, mutableStateOf 등 Compose에 대한 이해가 필요하실 경우 공식문서 추천 드립니다.
https://developer.android.com/jetpack/compose/state?hl=ko
https://developer.android.com/jetpack/compose/side-effects?hl=ko
'Android' 카테고리의 다른 글
[Android] Compose Scrollable Column에서 처음 보여지는 타이밍 캐치하기 (0) | 2024.07.05 |
---|---|
[Android] Fragment.rootFragment extension property (1) | 2023.11.20 |
[개발 정보] 코틀린 코루틴 레시피(활용법) (0) | 2023.04.24 |
[Android] Gson을 대체하는 Moshi (0) | 2023.03.10 |
[Android] Hilt: Dependency Injection Made Easy (0) | 2023.02.26 |