Open lee-ji-hoon opened 2 months ago
Kotlin StateFlow는 프로그램에서 상태를 관리하는데 사용되는 Kotlin 라이브러리이다. StateFlow는 여러 코루틴에서 공유될 수 있는 State Holder이다.
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
우선 emit, value, update 방식이 존재한다.
override suspend fun emit(value: T) {
this.value = value
}
@Suppress("UNCHECKED_CAST")
public override var value: T
get() = NULL.unbox(_state.value)
set(value) { updateState(null, value ?: NULL) }
_state
가 관리되고 있다._state
는 atomic으로 set()
내의 updateState()
함수를 통해 계속 변화되고 있다._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")
}
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
compareAndSet()
함수 즉 CAS알고리즘을 사용하고 잇다.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)
에 대해서 살펴보자
코루틴 시작 전략 설정
val start = if (started == SharingStarted.Eagerly) CoroutineStart.DEFAULT else CoroutineStart.UNDISPATCHED
코루틴 시작
return launch(context, start = start) { // the single coroutine to rule the sharing
공유 시작 전략에 따른 데이터 수집 코루틴 내부에서 started 전략에 따라 데이터 수집 방식을 결정한다.
Eagerly 전략
when {
started === SharingStarted.Eagerly -> {
upstream.collect(shared)
}
Lazily 전략
started === SharingStarted.Lazily -> {
shared.subscriptionCount.first { it > 0 }
upstream.collect(shared)
}
기타 및 커스텀 전략
else -> {
started.command(shared.subscriptionCount)
.distinctUntilChanged()
.collectLatest {
when (it) {
SharingCommand.START -> upstream.collect(shared)
SharingCommand.STOP -> { /* do nothing */ }
SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> {
if (initialValue === NO_VALUE) {
shared.resetReplayCache()
} else {
shared.tryEmit(initialValue)
}
}
}
}
}
}
다른 및 커스텀 전략은 command를 통해 수신된 명령을 처리한다.
명령에 따라 다르게 동작한다:
START: 데이터를 수집.
STOP: 아무 작업도 하지 않다.
STOP_AND_RESET_REPLAY_CACHE: 재생 캐시를 초기화. initialValue가 설정된 경우, 초기값으로 캐시를 설정.
24, 25장