본문 바로가기

Android

[Android] Compose Flow.collectAsState() 파악하기

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 

 

Compose의 부수 효과  |  Jetpack Compose  |  Android Developers

Compose의 부수 효과 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 부수 효과는 구성 가능한 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항입니다.

developer.android.com