Jwhyee / kotlin-coroutine-study

코틀린 코루틴 북 스터디 📚
4 stars 1 forks source link

3부 채널과 플로우 - 4 #8

Open lee-ji-hoon opened 2 months ago

lee-ji-hoon commented 2 months ago

24, 25장

lee-ji-hoon commented 2 months ago

StateFlow

StateFlow의 정의부터 알고 가자!

Kotlin StateFlow는 프로그램에서 상태를 관리하는데 사용되는 Kotlin 라이브러리이다. StateFlow는 여러 코루틴에서 공유될 수 있는 State Holder이다.

StateFlow는 안정적이고 반응형으로 상태를 관리해야 하는 프로그램에서 유용하다.

StateFlow는 어떻게 구현이 돼 있을까?

우선 알아야할게 StateFlow는 SharedFlow의 한 종류로 상태를 공유하기 위한 상태 객체일 뿐이다.

public interface StateFlow<out T> : SharedFlow<T> {
    /**
     * The current value of this state flow.
     */
    public val value: T
}
// MutableStateFlow(initialValue) is a shared flow with the following parameters:
val shared = MutableSharedFlow(
    replay = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)
shared.tryEmit(initialValue) // emit the initial value
val state = shared.distinctUntilChanged() // get StateFlow-like behavior

그럼 StateFlow의 Data는 어떻게 갱신이 되길래 Thread-Safe 하다는 걸까?

우선 emit, value, update 방식이 존재한다.

emit

코드링크

override suspend fun emit(value: T) {
    this.value = value
}

value

코드링크

@Suppress("UNCHECKED_CAST")
public override var value: T
    get() = NULL.unbox(_state.value)
    set(value) { updateState(null, value ?: NULL) }

value로 업데이트 하는 예시를 들어보자!

_uiState.value = _uiState.value.copy(title = "Something")

하지만 copy가 완료 되고 StateFlow의 새 값이 전송될 때까지 다른 Thread가 접근하지 않을 것이라는 보장이 있나?

// Label: Launch A
viewModelScope.launch(Dispatchers.IO) {
    _uiState.value = _uiState.value.copy(doneButtonEnabled = true)
}

// Label: Launch B
viewModelScope.launch(Dispatchers.Default) {
    _uiState.value = _uiState.value.copy(title = "New Title")
}

update를 활용하자!

public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
    while (true) {
        val prevValue = value
        val nextValue = function(prevValue)
        if (compareAndSet(prevValue, nextValue)) {
            return
        }
    }
}
Jaeeun1083 commented 2 months ago

shardIn 함수 내부

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T> {
    val config = configureSharing(replay)
    val shared = MutableSharedFlow<T>(
        replay = replay,
        extraBufferCapacity = config.extraBufferCapacity,
        onBufferOverflow = config.onBufferOverflow
    )
    @Suppress("UNCHECKED_CAST")
    val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
    return ReadonlySharedFlow(shared, job)
}

scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)에 대해서 살펴보자

  1. 코루틴 시작 전략 설정

    val start = if (started == SharingStarted.Eagerly) CoroutineStart.DEFAULT else CoroutineStart.UNDISPATCHED
    • started가 Eagerly인 경우, 코루틴은 기본 방식 (DEFAULT)으로 시작합니다. 그렇지 않으면 UNDISPATCHED 방식으로 시작하여 즉시 실행되지 않도록 한다.
  2. 코루틴 시작

    return launch(context, start = start) { // the single coroutine to rule the sharing
    • 주어진 컨텍스트와 시작 전략을 사용하여 새로운 코루틴을 시작한다.
  3. 공유 시작 전략에 따른 데이터 수집 코루틴 내부에서 started 전략에 따라 데이터 수집 방식을 결정한다.


다른 및 커스텀 전략은 command를 통해 수신된 명령을 처리한다. 
명령에 따라 다르게 동작한다:

START: 데이터를 수집.
STOP: 아무 작업도 하지 않다.
STOP_AND_RESET_REPLAY_CACHE: 재생 캐시를 초기화. initialValue가 설정된 경우, 초기값으로 캐시를 설정.