Flow, StateFlow, SharedFlow

이윤설·2025년 3월 3일
0

안드로이드 연구소

목록 보기
32/33

State란?

State는 객체 지향 관점에서 자주 사용되는 단어로 객체가 특정 시점에서 어떤 데이터 값을 가지고 있는지 나타내는 것이다.

안드로이드의 UI 레이어 가이드에서는 UI 레이어의 UI State를 생성하고 관리하는 수단으로 단방향 데이터 흐름(UDF)을 설명한다.

이러한 State는 UI와 관련된 데이터를 저장하고 관리하는데 사용되는 상태 홀더인 ViewModel을 통해 관리된다.

상태 홀더인 ViewModel은 앱이 상태를 읽을 수 있도록 상태를 저장한다.
로직이 필요한 경우 필요한 로직을 호스팅하는 데이터 소스에 대한 액세스 권한을 제공한다.

LiveData란?

이러한 State 데이터를 다루기 위해 안드로이드에서는 LiveData를 사용했었다.
다만 LiveData는 액티비티의 LifeCycle에 반응해 데이터의 변경 사항을 관찰하고 이에 대응한다.

따라서 만약 액티비티가 있는 UI 레이어가 아닌 Domain 레이어와 상호작용한다면 기존의 LiveData를 사용할 수가 없어진다는 단점이 있다.

이럴 때 사용할 수 있는 API가 바로 Flow이다.

Cold Stream vs Hot Stream

Cold Stream은 Flow를 수집하는 각 Collector독립적인 데이터 스트림을 생성하여 개별적으로 데이터를 수집한다. 즉, Collector마다 새로운 데이터 흐름이 시작되며, 데이터를 수집(collect)하지 않으면 아무 작업도 수행되지 않는다.

ex) 사용자가 버튼을 눌러 데이터를 새로고침할 때마다 새로운 네트워크 요청을 보내는 경우.

반면, Hot Stream은 여러 Collector가 동일한 데이터 스트림을 공유하여 같은 데이터를 수집한다. 일부 구현에서는 Collector가 있을 때만 시작되지만, 기본적으로 Collector가 없어도 데이터 제공자(Provider)는 계속해서 데이터를 생성하고 스트림을 유지한다.

ex) UI 상태 관리 (예: Dark Mode 토글, 로그인 상태 등)


Flow: 우편함에서 직접 편지를 받는 구조 (Cold Stream)

Flow는 우편함에서 직접 편지를 가져오는 방식과 같다.
우체부가 새로운 편지를 보냈다고 해도, 내가 우편함을 열어보지 않으면(collect 하지 않으면) 절대 편지를 받을 수 없다.
또, 우편함을 열 때마다 처음부터 다시 새롭게 배달된다.

💡 Flow의 특징
✔️ 데이터를 하나씩 순차적으로 받는 구조 (ex: API 요청)
✔️ Cold Stream → 직접 요청해야만 데이터 수신 가능 (collect 필요)
✔️ 값을 유지하지 않음 → 현재 상태(state) 개념이 없음

fun getMessages(): Flow<String> = flow { 
    val messages = listOf("첫 번째 편지", "두 번째 편지", "세 번째 편지")
    for (msg in messages) {
        delay(100) 
        emit(msg) // 데이터를 방출
    }
}

fun main() = runBlocking {
    getMessages().collect { println(it) } 
    // 직접 우편함을 열어야(collect) 편지를 받을 수 있음
}
  • 우편함을 열면(collect) "첫 번째 편지", "두 번째 편지", "세 번째 편지" 순서대로 받음
  • 다시 열면 또 처음부터 같은 내용이 배달됨

StateFlow: 언제든지 현재 상태를 볼 수 있는 전광판 (Hot Stream)

StateFlow는 항상 최신 정보를 표시하는 전광판과 같다.
전광판은 항상 최신 정보를 유지하고 있어서 새로 온 사람도 현재 상태를 바로 볼 수 있다.

💡 StateFlow의 특징
✔️ 현재 상태(state)를 항상 유지 (초기값 필요)
✔️ Hot Stream → 구독하지 않아도 최신 값 유지
✔️ .value 사용 가능 → 언제든지 현재 상태 확인 가능

class MyViewModel : ViewModel() {
    private val _stateFlow = MutableStateFlow("기본 상태") // 전광판에 처음 표시될 내용
    val stateFlow = _stateFlow.asStateFlow()

    fun updateState(newText: String) {
        _stateFlow.value = newText // 전광판의 내용을 변경
    }
}
lifecycleScope.launchWhenStarted {
    myViewModel.stateFlow.collectLatest {
        binding.textView.text = it // 전광판이 바뀌면 UI도 자동으로 변경됨
    }
}

collectLatest를 사용하면 최신 상태만 갱신됨


SharedFlow: 라디오 방송 (Hot Stream, 이벤트 기반)

SharedFlow는 라디오 방송처럼 특정 순간에만 송출되는 데이터 스트림이다.
라디오를 켜면(collect 하면) 지금 방송 중인 내용을 들을 수 있지만,
라디오를 늦게 켜면 이미 지난 방송은 들을 수 없다.

💡 SharedFlow의 특징
✔️ 여러 구독자에게 동시에 데이터 전송 가능
✔️ 이전 값 유지 X.value 불가능 (이벤트성 데이터에 적합)
✔️ 버퍼 설정 가능 → 필요하면 일정 시간 동안 데이터를 보관 가능

class MyViewModel : ViewModel() {
    private val _snackbarFlow = MutableSharedFlow<String>()
    val snackbarFlow = _snackbarFlow.asSharedFlow()

    fun showSnackbar(message: String) {
        viewModelScope.launch {
            _snackbarFlow.emit(message) // Snackbar 메시지 이벤트 발생
        }
    }
}
---------------------------------------------
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel(), scaffoldState: ScaffoldState) {
    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        viewModel.snackbarFlow.collect { message ->
            coroutineScope.launch {
                scaffoldState.snackbarHostState.showSnackbar(message) // Snackbar 표시
            }
        }
    }

    Scaffold(
        scaffoldState = scaffoldState,
        content = {
            Button(onClick = { viewModel.showSnackbar("버튼이 눌렸어요!") }) {
                Text("Snackbar 띄우기")
            }
        }
    )
}

비교 정리

개념예시Cold/Hot현재 값 유지사용 예
Flow우편함Cold❌ 없음리스트 가져오기
StateFlow전광판Hot✅ 있음UI 상태 유지
SharedFlow라디오 방송 송출Hot❌ 없음이벤트 발생

🛠 어떤 걸 써야 할까?

  1. 단순히 데이터를 가져와서 UI에 표시하고 싶다면?
    Flow

  2. 현재 상태를 기억하고 싶다면?
    StateFlow

  3. 이벤트(알림, 메시지) 처리에 사용하고 싶다면?
    SharedFlow


StateFlow Vs. MutableStateFlow

차이점 설명

  1. StateFlow: 읽기 전용 (값을 가져올 수는 있지만 값을 수정할 수는 없음)
  2. MutableStateFlow: 읽기/쓰기가 가능 (값을 가져오고 수정할 수 있음)

예시 코드

1. StateFlow (읽기 전용)

class MyViewModel : ViewModel() {
    private val _stateFlow = MutableStateFlow(0)  // 내부에서 상태를 변경 가능
    val stateFlow = _stateFlow.asStateFlow()     // 외부에서는 읽기 전용 (값 수정 불가)
    
    fun increment() {
        _stateFlow.value += 1  // 내부에서만 값 변경
    }
}
  • stateFlow는 외부에서 읽기만 가능하다. 값을 가져오지만 변경할 수 없다. 따라서 외부에서는 단지 현재 값을 관찰하거나 collect 할 수만 있다.

2. MutableStateFlow (읽기/쓰기 가능)

class MyViewModel : ViewModel() {
    val mutableStateFlow = MutableStateFlow(0)  // 읽기와 쓰기 모두 가능

    fun increment() {
        mutableStateFlow.value += 1  // 값을 직접 변경 가능
    }
}
  • mutableStateFlow는 외부에서 읽고 쓰기가 가능하다.
    값을 가져오는 것뿐만 아니라 외부에서 직접 값을 수정할 수도 있다.

요약

  • StateFlow: 읽기 전용이다. 값이 변경되면 collect를 통해 변경된 값을 감지할 수 있다. 외부에서는 값을 변경할 수 없고, 내부에서만 수정 가능하다.
  • MutableStateFlow: 읽기/쓰기가 가능하다. 외부에서도 값을 읽고, 수정할 수 있으며, 내부에서 상태를 변경할 때 사용된다.

따라서, StateFlow외부에 공개된 상태를 나타내는 용도로 사용하고,
MutableStateFlow상태를 관리하고 변경하는 용도로 사용한다.


Snackbar 예시

MainActivity에서 StateFlow의 상태를 변화시키고 이를 collect 한 다음 SnackBar를 출력하는 코드가 있다고 가정하겠다.
버튼을 클릭하면 스낵바를 출력할 수 있다.
하지만 버튼을 다시 클릭한다고 해도 스낵바는 생성되지 않는다. 상태가 변하지 않기 때문이다.

그런데 화면이 회전하는 등의 행위로 액티비티가 재생성 된다면, StateFlow는 자동으로 value를 다시 emit 해주게 된다. 그래서 스낵바가 생성된다.
이러한 StateFlow의 특성은 스낵바와 같은 일회성 작업을 하고 싶을 때는 적합하지 않다.

즉, StateFlow는 새로운 값이 들어와야만 UI가 갱신되기 때문에,
Snackbar를 띄우는 stateFlow.collectLatest { } 코드는 값이 변할 때만 실행된다.

같은 값을 다시 emit하면 UI가 갱신되지 않는다!

SharedFlow는 어떨까?
SharedFlow는 상태가 아닌 이벤트를 전달하기에 버튼을 몇번이나 누르던 스낵바를 생성한다. 그리고 액티비티가 재생성 될때에는 스낵바를 생성하지 않는다.
SharedFlow는 과거의 데이터를 저장하지 않기 때문이다.
즉, 이전 이벤트를 기억하지 않기 때문에 화면이 회전해도 새롭게 emit되지 않는다.
결과적으로 Snackbar가 다시 나타나지 않는 것이다.
이는 SharedFlow가 One-Time 이벤트에 매우 적합하다는 것을 의미한다.

정리

StateFlowSharedFlow
핵심 개념최신 상태 유지 (State)새로운 이벤트 전송 (Event)
Snackbar 실행값이 변할 때만이벤트 발생할 때마다
같은 값 emit 시 UI 변화X (변화 없음)O (항상 실행됨)
화면 회전 후 동작기존 값 다시 emit → Snackbar 뜸기존 이벤트 기억 X → Snackbar 안 뜸
주요 사용 예시UI 상태 유지 (ex. 버튼 활성화 여부)One-time Event (ex. 메시지 알림)

출처
https://coding-juuwon2.tistory.com/349
https://developer.android.com/kotlin/flow?hl=ko#modify
https://everyday-develop-myself.tistory.com/306

profile
화려한 외면이 아닌 단단한 내면

0개의 댓글

관련 채용 정보