👩💻 오늘의 할 일
그동안 저는 SateFlow가 같은 값을 방출하지 않도록 설계되어 같은 값이 필요할 경우 SharedFlow를 사용해야 한다고 알고 있었습니다. 오늘은 StateFlow가 왜 같은 값을 방출하지 않는지 낱낱이 파헤쳐 봅시다!
distinctUntilChanged
StateFlow는 distinctUntilChanged와 같은 연산을 합니다. 먼저 distinctUntilChanged에 대해 알아보겠습니다.
먼저 함수명 그대로 해석을 해보겠습니다. distinct는 학교에서 DataBase를 배울 때 중복을 제거하고 데이터를 가져올 때 사용했던 기억이 나네요. 그럼 이어 해석을 해보면 distinct Until Changed 즉, 변경될 때까지 중복을 제거한다라고 해석할 수 있겠네요!
실제로도 맞는지 공식문서에서 확인해 보겠습니다. distinctUntilChanged는 코틀린 공식 문서에 Flow의 확장 함수로 선언되어 있습니다.
내 친구 파파고의 힘을 빌려 해석해 볼까요? 도와줘 파파고야 ~
중요한 구문은 이 부분 같네요 distinctUntilChanged은 동일한 값이 반복되면 모두 필터링되는 흐름을 리턴합니다. StateFlow는 distinctUntilChanged 연산자가 적용된 것처럼 동작합니다. 내부 구현을 살펴보니 아예 친절하게 StateFlow는 중복을 제거한다고 친절하게 나와있네요?
2️⃣ distinctUntilChanged 함수의 내부 구현
코드 구조 분해
코드를 보기 쉽게 만들기 위해 코틀린 파일을 만들어 하나씩 살펴보겠습니다.
private val defaultKeySelector: (Any?) -> Any? = { it }
private val defaultAreEquivalent: (Any?, Any?) -> Boolean = { old, new -> old == new }
public fun <T> Flow<T>.distinctUntilChanged(): Flow<T> =
when (this) {
is StateFlow<*> -> this // state flows are always distinct
else -> distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = defaultAreEquivalent)
}
@Suppress("UNCHECKED_CAST")
public fun <T> Flow<T>.distinctUntilChanged(areEquivalent: (old: T, new: T) -> Boolean): Flow<T> =
distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = areEquivalent as (Any?, Any?) -> Boolean)
public fun <T, K> Flow<T>.distinctUntilChangedBy(keySelector: (T) -> K): Flow<T> =
distinctUntilChangedBy(keySelector = keySelector, areEquivalent = defaultAreEquivalent)
private fun <T> Flow<T>.distinctUntilChangedBy(
keySelector: (T) -> Any?,
areEquivalent: (old: Any?, new: Any?) -> Boolean
): Flow<T> = when {
this is DistinctFlowImpl<*> && this.keySelector === keySelector && this.areEquivalent === areEquivalent -> this // same
else -> DistinctFlowImpl(this, keySelector, areEquivalent)
}
private class DistinctFlowImpl<T>(
private val upstream: Flow<T>,
@JvmField val keySelector: (T) -> Any?,
@JvmField val areEquivalent: (old: Any?, new: Any?) -> Boolean
): Flow<T> {
override suspend fun collect(collector: FlowCollector<T>) {
var previousKey: Any? = NULL
upstream.collect { value ->
val key = keySelector(value)
@Suppress("UNCHECKED_CAST")
if (previousKey === NULL || !areEquivalent(previousKey, key)) {
previousKey = key
collector.emit(value)
}
}
}
}
1. 람다식 선언
- defaultKeySelector : 전달받은 값을 반환하는 람다식으로 중복을 검사할 기준값입니다.
- defaultAreEquivalent : 두 개의 인자를 전달받아 두 인자가 같은지 반환
2. Flow <T>를 반환하는 두 개의 distinctUntilChanged 확장 함수 선언
- 인자가 없는 distinctUntilChanged
- 수신 객체 타입에 따라 분기합니다.
- StateFlow Type일 경우 수신 객체 타입을 반환합니다. 단, 중복된 값은 제거됩니다.
- StateFlow Type이 아닐 경우 distinctUntilChangedBy 함수를 호출합니다.
- 위에서 선언한 두 개의 람다 함수를 인자로 전달합니다.
- 수신 객체 타입에 따라 분기합니다.
- 람다식을 인자로 받는 distinctUntilChanged
- 두 개의 인자를 받고 Boolean을 반환하는 람다식을 인자로 받습니다.
- distinctUntilChangedBy 함수를 호출합니다.
- 위에서 선언한 두 개의 람다 함수를 인자로 전달합니다.
3. DistinctFlowImpl
- 클래스의 인스턴스를 생성하여 반환 (Flow) 하는 함수입니다.
- 이 함수는 collect 함수를 오버라이드 함으로써 새로운 Flow를 생성하고 반환해 입력된 원본 Flow의 요소를 처리하는 방법을 변경합니다.
- 입력된 원본 Flow란 위에서 계속해서 언급된 수신 객체 타입(Flow)입니다. 여기선 인자 중 upstream이 되겠네요!
- upstream.collect -> 인자로 전달받은 수신 객체 타입(Flow)을 collect 해서
- val key = keySelector(value)로 전달해 전달받은 값 자체를 반환하는 람다식 객체를 생성합니다.
- keySelector는 맨 처음 선언한 defaultKeySelector입니다! 처음부터 계속해서 함수를 호출하며 인자로 전달해 여기까지 왔네요
- 조건문을 통해 객체의 주소값을 비교하고(NULL) 수신 객체 타입이 중복된 값인지 확인하여 다른 값이라면 previousKey에 담아 emit( ) 합니다.
- === -> 객체의 주소 비교, == -> 객체의 값을 비교
4. Flow <T>를 반환하는 distinctUntilChangedBy 확장 함수 선언
- 두 가지 케이스로 분기하여 Flow를 반환합니다.
- this is DistinctFlowImpl <*>: 현재의 Flow가 DistinctFlowImpl 클래스의 인스턴스인지 확인
- this.keySelector === keySelector: 현재의 Flow와 매개변수로 전달된 keySelector와 동일한지 확인
- this.areEquivalent === areEquivalent: 현재의 Flow와 전달된 동등성 비교 함수(areEquivalent)의 주소값과 매개변수로 전달된 Flow의 동등성 비교 함수의 주소가 동일한지 확인
- 이 세 가지 조건이 모두 참인 경우, 즉 현재의 Flow가 이미 원하는 중복 제거 기준을 사용하고 있다면, 원래의 Flow를 그대로 반환합니다. 그렇지 않은 경우, 새로운 중복 제거된 Flow를 생성하여 반환합니다.
🙇♂️ 후기
앞으로 블로그를 쓰면서 공부도 단순히 겉햝기가 아니라 내부 구조를 한번 파보는 식으로 해보려고 합니다. 구조 파악도 있지만 지금 안드로이드를 개발하면서 어려운 코틀린을 문법들 사용해 보기가 쉽지 않아서 우선 눈으로라도 파악해 보자라는 취지에서 시작하게 되었습니다.
'KOTLIN' 카테고리의 다른 글
[Kotlin]Sealed Class란 무엇일까 ? (0) | 2024.03.22 |
---|---|
Coroutine SharedFlow (0) | 2024.03.09 |
[Kotlin] LiveData 대신 StateFlow 사용하기 (1) | 2024.01.15 |
[Kotlin] Coroutine Flow (0) | 2024.01.15 |
[KOTLIN IN DEPTH] 구조적 동시성과 코루틴 문맥 (1) | 2023.12.18 |