본문 바로가기
Kotlin 공부 노트

[Kotlin Flow] StateFlow vs SharedFlow 예제와 함께 차이점 알아보기!

by 지게요 2024. 11. 30.
728x90
반응형

이번 포스팅 공부 할 내용은 Flow 중에서도 StateFlow, SharedFlow를 공부해 보겠다.

만약 Flow의 개념을 모른다면 링크에서 보고 오는 것을 추천한다.

 

# StateFlow vs SharedFlow

둘 다 Hot Stream이지만 용도에 맞게 사용해야 더 좋은 퍼포먼스를 낼 수 있기에 각각의 차이점을 알고 구분해서 사용하는 것이 중요하다.

## StateFlow

### 초기값이 필수적

private val _mainState: MutableStateFlow<MainState> = MutableStateFlow(MainState.Loading)
val mainState = _mainState

 

보통 ViewModel에서 위와 같이 사용할 텐데 초기값을 필수적으로 적어줘야 한다.

만약 초기값을 안 주면 아래와 같은 오류 메시지가 나온다.

해당 메시지를 번역해 보면"주어진 초기값으로 MutableStateFlow를 만듭니다."라고 나오기 때문에 초기값은 필수이다.


위 사진은 MutableStateFlow의 내부 구현인데 value는 제네릭이기 때문에 
그 값이 반드시 non-null일 필요는 없다.

하지만 명확성과 유지보수 측면에서 null 사용을 권장하지 하지 않는다.

### 항상 최신 값 하나만 유지

StateFlow는 항상 최신 값 하나만 유지한다.

아래 예제는 MutableStateFlow Value에 값을 넣어준 후 Value값을 출력해 보는 예제이다.

val exStateFlow: MutableStateFlow<Int> = MutableStateFlow(0)

println("초기 exStateFlow ${exStateFlow.value}")

exStateFlow.value = 1 // 이전 값 0 사라짐
println("1대입 exStateFlow ${exStateFlow.value}")

exStateFlow.value = 2 // 이전 값 1 사라짐
println("2대입 exStateFlow ${exStateFlow.value}")

exStateFlow.value = 3 // 이전 값 2 사라짐
println("3대입 exStateFlow ${exStateFlow.value}")

 

출력 결과

 

예제 출력에서 보다시피 Value에 값을 넣어주면 이전 값들은 사라지고 최신 값 하나만 유지하게 되는 특성을 볼 수 있다. 

### 중복 방출 된 값을 무시한다.

StateFlow는 distinctUntilChanged 속성이 true를 기본적으로 설정되어 있기 때문에 중복 방출 된 값을 무시한다.

아래 예제에서는 MutableStateFlow Value에 중복값을 넣어준 후 Value값을 출력해 보는 예제이다.

 

val exStateFlow: MutableStateFlow<Int> = MutableStateFlow(0)
// StateFlow 구독
launch {
    exStateFlow.collect { value ->
        println("방출된 StateFlow 값: $value")
    }
}
delay(100)

exStateFlow.value = 1
delay(100)

exStateFlow.value = 1  // 중복
delay(100)

exStateFlow.value = 2
delay(100)

exStateFlow.value = 2  // 중복
delay(100)

exStateFlow.value = 1  // 이전 값과 다름
delay(100)

 

println 결과가 어떤지 먼저 생각해 보고 아래 내려서 출력 결과를 한번 보면 좋을 거 같다.







 

위 예제의 출력은 이렇게 0,1,2,1 이 출력되는 걸 확인 함으로써 중복 방출 값을 무시하는 특성을 볼 수 있다. 

● 물론 distinctUntilChanged 속성을 false로 비활성화해 주면 되긴 하는데 권장하지 않는다.

### StateFlow 그럼 언제 사용 하나?

- UI 상태 관리

// UI 상태의 경우
data class UiState(
    val isLoading: Boolean = false,
    val data: List<String> = emptyList(),
    val error: String? = null
)

val uiState = MutableStateFlow(UiState()) // 초기 상태 필수

 

- 설정 값 관리

// 설정 값의 경우
val darkMode = MutableStateFlow(false) // 기본값 지정 가능

 

 

- 현재 상태가 중요한 경우

//토글 상태
class ToggleViewModel : ViewModel() {
    private val _isExpanded = MutableStateFlow(false)
    val isExpanded = _isExpanded.asStateFlow()

    fun toggleExpand() {
        _isExpanded.value = !_isExpanded.value
    }
}

## SharedFlow

### 초기값은 옵션

SharedFlow는 StateFlow와 다르게 초기값이 필수가 아닌 옵션이다.

보통 ViewModel에서 아래와 같이 사용할 텐데 초기값이 없어도 동작한다.

private val _mainEvent: MutableSharedFlow<FlowEvent> = MutableSharedFlow()
val mainEvent: SharedFlow<FlowEvent> = _mainEvent

sealed class FlowEvent {
    data class ShowSnackbar(val msg: String) : FlowEvent()
}

 

### 여러 값 유지 가능

ShardFlow는 replay 캐시를 통해 여러 값을 유지할 수 있다.

아래는 MutableSharedFlow의 내부 정의이다.

@Suppress("FunctionName", "UNCHECKED_CAST")
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
    require(replay >= 0) { "replay cannot be negative, but was $replay" }
    require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" }
    require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) {
        "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow"
    }
    val bufferCapacity0 = replay + extraBufferCapacity
    val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow
    return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow)
}

 

매개변수들을 보면 replay라는 매개변수가 있다. 바로 저 변수를 활용해 여러 값을 유지할 수 있다.

기본으로는 0이 되어있으며 기존에 있는 값을 유지하지 않는다.(최신 값 유지)

아래 예제 코드를 통해 좀 더 살펴보자!

val exSharedFlow = MutableSharedFlow<Int>(replay = 2)

exSharedFlow.emit(1)
exSharedFlow.emit(2)
exSharedFlow.emit(3) // 1, 2 값이 유지됨

 // replayCache를 사용해 캐시 값 가져오기
println("현재 캐시된 값들: ${exSharedFlow.replayCache}")

예제에서 보다시피 replay를 2로 설정해 주니 캐시 된 값이 2,3이 나온다.

만약 여기서 replay를 3으로 설정해 주면 결과가 어떨까? 

그러면 당연히 1,2,3이 나오게 된다. 따라서 ShardFlow는 replay 캐시를 통해 여러 값을 유지할 수 있다.

### 중복 값도 모두 방출한다

SharedFlow는 StateFlow와 다르게 distinctUntilChanged 속성이 없다.

따라서 중복 값도 모두 방출한다.

 

물론 아래 코드처럼 Flow 연산자를 사용해서 SharedFlow에도 distinctUntilChanged 동작을 추가 할 수 있다.

// distinctUntilChanged 적용
launch {
    sharedFlow
        .distinctUntilChanged() // 연속된 중복 값 제거
        .collect { value ->
            println("중복 제거된 값: $value")
        }
}

 

아래 예제를 통해 확인해 보자 SharedFlow에 중복값을 넣어준 후 값을 출력해 보는 예제이다.

 

val exSharedFlow: MutableSharedFlow<Int> = MutableSharedFlow()
        // exSharedFlow 구독
        launch {
            exSharedFlow.collect { value ->
                println("방출된 SharedFlow 값: $value")
            }
        }
        delay(100)

        exSharedFlow.emit(1)
        delay(100)

        exSharedFlow.emit(1)  // 중복
        delay(100)

        exSharedFlow.emit(2)
        delay(100)

        exSharedFlow.emit(2)  // 중복
        delay(100)

        exSharedFlow.emit(1)  // 이전 값과 다름
        delay(100)

 

StateFlow와 마찬가지로 println 결과가 어떤지 먼저 생각해 보고 아래 내려서 출력 결과를 한번 보면 좋을 거 같다.







 


출력에서 보다시피 중복값도 모두 방출이 된다.

### SharedFlow 그럼 언제 사용 하나?

- 일회성 이벤트 처리

private val _toastEvent = MutableSharedFlow<String>()
    val toastEvent = _toastEvent.asSharedFlow()

    fun saveData() {
        viewModelScope.launch {
            // 데이터 저장 후
            _toastEvent.emit("저장되었습니다")
            // 추가 처리 필요 없음
        }
    }

 

- 여러 곳에서 동시에 수신할 때

lass MessageService {
    private val _newMessages = MutableSharedFlow<Message>()
    val newMessages = _newMessages.asSharedFlow()

    suspend fun broadcastMessage(message: Message) {
        _newMessages.emit(message)
    }
}

// 여러 화면에서 동시에 구독
class ChatFragment : Fragment() {
    // 채팅 화면에 메시지 표시
    messageService.newMessages.collect { message ->
        showMessage(message)
    }
}

class NotificationWorker {
    // 동시에 알림으로도 표시
    messageService.newMessages.collect { message ->
        showNotification(message)
    }
}

 

# 총 정리

특성 StateFlow SharedFlow
초기값 필수 선택적
최신값 유지 항상 유지 replay 값에 따라 결정 (0: 유지X, 1이상: 유지)
중복값 처리 동일한 값은 무시 모든 값 발행
주요 용도 UI 상태 관리, 설정값 관리 일회성 이벤트, 실시간 데이터 스트림

# Flow vs StateFlow, SharedFlow

번외로 일반 Flow와 StateFlow, SharedFlow 이 두 가지의 큰 차이점을 간단하게 알아보자.

## Flow 

- Cold Stream / 수집하는 Observer가 있을 때만 데이터를 방출

- 이전에 방출된 값들을 저장하지 않음

## StateFlow, SharedFlow

Hot Stream / 수집하는 Observer가 없는 경우에도 데이터를 방출

- 최신 상태값을 항상 저장

 

 

 

반응형