본 글은 StateFlow 와 SharedFlow 를 보고 정리한 글입니다

상태를 나타내는 StateFlow


UI 를 다루는 상태를 생각해보자. 로그인 여부에 따라 “로그인”, “로그아웃” 등 상태를 정의할 수 있으며 화면에 표시하는 데이터도 “로딩 중”, “데이터 이용 가능”, “오류” 등의 상태로 정의할 수 있다.

그리고 한 가지 상태만을 가지고 있다.

Kotlin 으로 상태를 표현하는 방법은 부가적인 데이터가 필요 없다면 Enum 으로, 부가적인 데이터가 필요하다면 Sealed class 로 정의하기도 한다.

Android 에서는 이런 상태들을 다루기 위한 LiveData 를 제공한다. LiveData 는 Android lifecycle 을 알고 있기 때문에 자연스럽게 연동된다. 하지만 LiveData 는 생명 주기가 있는 UI와 연동되도록 설계되었기 때문에 비지니스 레이어에서 사용하는 것은 무리가 있다. 특히 LiveData 는 AAC 에서 제공되는 기능이기 때문에 비지니스 레이어를 플랫폼 독립적으로 가져가기 위해서 걸림돌이 된다.

과거에는 이 점을 RxJava 를 통해 해결했는데 이제는 코틀린을 주 언어로 사용하면서 경량화된 Reactive stream 구현체라고 볼 수 있는 Flow 를 이용하여 이를 구현할 수 있다.

Flow 글에서 설명한 것처럼 플로우는 기본적으로 콜드 스트림이다. 하지만 StateFlow, SharedFlow 와 같은 핫스트림 플로우도 제공된다.

이 중에서 기본 값을 가지고 모든 구독자에게 최신 상태를 전달해주는 것은 StateFlow 이다.

// code by Myungpyo Shim
@HiltViewModel
class MainViewModel(
    private val refreshDataUseCase: RefreshDataUseCase,
    @Assisted private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val _dataState: MutableStateFlow<UiState<List<MembershipUiModel>>> 
        = MutableStateFlow(UiState.InProgress())
    val dataState = _dataState.asStateFlow()


    fun refreshData() = viewModelScope.launch {
        runCatching {
            refreshDataUseCase.execute()
        }.onSuccess { data ->
            _dataState.emit(UiState.Success(data))
        }.onFailure { throwable ->
            _dataState.emit(UiState.Fail(throwable))
        }
    }
}

_dataState 와 dataState 를 정의하는 부분을 보면 _dataState는 Mutable Flow 이고, dataStore 는 Immutable Flow 로 정의된다.

refreshData() 가 호출되면 ViewModel 스코프에서 코루틴이 생성되어 UseCase 의 중단 함수를 호출하고, 결과(성공/실패)에 따라서 적당한 데이터로 변환하여 Flow 를 전달하고 dataState 를 구독하고 있는 UI를 갱신할 수 있게 만든다.

이벤트를 나타내는 SharedFlow


위에서 살펴본 것과 같이 애플리케이션은 상태에 따라 전환하며 실행된다. 상태를 담고 있는 상태 머신은 항상 기본 상태를 갖고 동작하지만 이벤트는 기본 값 없이 특정 상황이 발생하면 구독자들에게 이벤트라는 형태로 전달하게 된다.

상태와 이벤트는 어떻게 다른 걸까

  • 상태 : 기본 값이 있고, 신규 구독 시 가장 최근 값을 받는다
  • 이벤트 : 기본 값이 없고, 구독 이후에 발생한 값을 받는다

이벤트는 일반적으로 UI Layer 에서 사용자와 뷰 간의 인터렉션에서 발생한 이벤트를 전달하거나 시스템에 메모리 부족 이벤트, 인증 오류 발생 이벤트 등 관련 컴포넌트들이 대응할 수 있도록 하기 위해서 사용된다.

// code by Myungpyo Shim
@HiltViewModel
class MainViewModel(
    @Assisted private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val _systemEvent: MutableSharedFlow<Unit> =
        MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
    val systemEvent = _systemEvent.asSharedFlow()
    
    init {
        viewModelScope.launch {
            systemEvent.collect { systemEvent ->
                when(systemEvent) {
                    is SystemEvent.MemoryWarning -> { TODO() }
                    is SystemEvent.StorageWarning -> { TODO() }
                    else -> { }
                }
            }
        }
    }
    
    fun reportSystemEvent(systemEvent: SystemEvent) {
        _systemEvent.emit(systemEvent)
    }
}

구조는 StateFlow 와 비슷하지만 MutableSharedFlow 를 만들 때 몇 가지 옵션을 통해 SharedFlow 동작을 재정의 한다.

  • replay = 0 : 새로운 구독자에게 이전 이벤트를 전달하지 않음
  • extraBufferCapacity = 1 : 추가 버퍼를 생성하여 emit 한 데이터가 버퍼에 유지되도록 함
  • onBufferOverFlow = BufferOverflow.DROP_OLDEST : 버퍼가 꽉 찼을 때 오래된 데이터 제거

계층 구조

내부적으로 다음과 같은 계층 구조를 가진다.

Flow < SharedFlow < StateFlow

SharedFlow 에서는 replayCache 와 크기를 정의하여 새로운 구독자에게 반복 전달할 값의 개수를 설정할 수 있고, 구독자들은 Slot 이라는 형태로 관리하여 값이 전달될 때 액티브한 모든 구독자에게 새로운 값이 전달되도록 한다.

SharedFlow 를 상속하는 StateFlow 는 기본 값을 가지고 있으며 replaceCache 는 가장 최근 값 하나를 갖는 리스트를 replayCache 로 재정의한다.