Flow·StateFlow·SharedFlow — 비동기 데이터 스트림
코루틴의 suspend가 값 하나를 비동기로 돌려준다면, Flow는 여러 값을 시간차로 흘려보내는 비동기 스트림이다 — 검색어 입력, DB 변경 구독, 위치 업데이트처럼 연속해서 도착하는 데이터에 쓴다. 안드로이드의 *상태(StateFlow)·이벤트(SharedFlow)*가 모두 Flow 위에 서 있어, MVVM·단방향 데이터 흐름의 핵심 배관이다. (출처: Kotlin Docs — Flow / Android — StateFlow & SharedFlow.)
Flow 기본 — cold 스트림
flow { } 빌더 안에서 emit()으로 값을 방출하고, collect(suspend 최종 연산)로 받는다. Flow는 cold — collect할 때 비로소 실행되고, 구독자마다 처음부터 새로 흐른다.
fun numbers(): Flow<Int> = flow {
for (i in 1..3) { delay(100); emit(i) } // 값을 하나씩 방출
}
scope.launch {
numbers().collect { value -> // collect해야 흐르기 시작 (cold)
println(value) // 1, 2, 3 (시간차)
}
}
연산자 — 코루틴 위의 함수형 파이프라인
Flow도 컬렉션처럼 연산자를 체이닝한다. 단, 모두 코루틴 안에서 비동기로 돈다.
searchQuery
.debounce(300) // 입력이 멎고 300ms 후에만 (타이핑 폭주 억제)
.filter { it.length >= 2 }
.distinctUntilChanged() // 같은 값 연속이면 무시
.flatMapLatest { q -> // 새 검색어 오면 이전 검색 취소하고 최신만
repository.search(q) // Flow 반환
}
.flowOn(Dispatchers.IO) // 위(upstream) 연산을 IO에서
.catch { e -> emit(emptyList()) } // 예외를 스트림 안에서 처리
.collect { results -> render(results) }
flowOn은 위쪽(upstream) 연산의 디스패처를 바꾸고(아래는 collect한 곳), catch는 상류 예외를 잡으며, combine/zip은 여러 Flow를 합친다. debounce·flatMapLatest는 검색·입력 처리의 교과서 조합이다.
cold vs hot — Flow와 StateFlow/SharedFlow
기본 Flow는 cold(구독해야 시작, 구독자마다 독립)지만, StateFlow·SharedFlow는 hot — 구독자와 무관히 존재하고 여러 구독자가 같은 방출을 공유한다. 그래서 화면 상태·이벤트에 적합하다.
StateFlow — 화면 '상태'의 단일 출처
StateFlow는 항상 현재값 하나를 들고 있는 hot Flow다(상태 홀더). value로 즉시 현재값을 읽고, 값이 바뀌면 구독자에게 흘린다. 같은 값은 방출 안 함(conflated·distinct). MVVM에서 ViewModel이 화면 상태를 StateFlow로 노출하는 게 표준이다.
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow() // 밖엔 읽기 전용
fun load() = viewModelScope.launch {
_uiState.value = UiState.Loading
runCatching { repo.load() }
.onSuccess { _uiState.value = UiState.Success(it) }
.onFailure { _uiState.value = UiState.Error(it.message ?: "") }
}
}
StateFlow vs LiveData — 둘 다 생명주기 인지 관찰 가능한 상태 홀더지만, StateFlow는 코루틴/Flow 생태계에 통합되고 초기값이 필수이며 플랫폼 독립(순수 코틀린)이다. 구글은 신규 코드에 StateFlow를 권장한다.
SharedFlow — 일회성 '이벤트'
화면 이동·스낵바·토스트 같은 일회성 이벤트는 StateFlow(상태)로 표현하면 회전 후 재방출 등 문제가 생긴다. SharedFlow는 상태가 아닌 이벤트 스트림으로, replay/buffer를 조절해 여러 구독자에게 방출한다.
private val _events = MutableSharedFlow<UiEvent>() // replay=0: 새 구독자에 과거 X
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onSaved() = viewModelScope.launch { _events.emit(UiEvent.NavigateBack) }
| StateFlow | SharedFlow | |
|---|---|---|
| 성격 | 상태(현재값 1개) | 이벤트 스트림 |
| 초기값 | 필수 | 없음 |
| 중복 | conflate(같은 값 무시) | 설정대로 방출 |
| 용도 | UI 상태 | 네비게이션·토스트 |
안드로이드에서 안전하게 수집
Flow를 화면에서 수집할 땐 생명주기에 맞춰야 한다 — 백그라운드에서도 수집하면 낭비·크래시다. repeatOnLifecycle(STARTED)로 화면이 보일 때만 수집하고 안 보이면 취소한다([andsys] 코루틴 on Android에서 자세히).
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { render(it) } // STARTED일 때만 수집
}
}
stateIn/shareIn으로 cold Flow를 hot StateFlow/SharedFlow로 변환해 여러 구독자가 공유하게 만든다(SharingStarted.WhileSubscribed).
정리 — 스트림으로 상태와 이벤트를 흘린다
Flow의 핵심 — 여러 비동기 값을 흘리는 cold 스트림(연산자로 변환), StateFlow는 현재값 하나를 들고 UI 상태를 단일 출처로, SharedFlow는 일회성 이벤트. 안드로이드에선 ViewModel이 StateFlow로 상태를 노출하고, 화면은 repeatOnLifecycle로 안전하게 수집한다. 이 배관이 단방향 데이터 흐름(UDF)([andarch])을 가능케 한다.
한 줄 요약 — Flow=여러 값 비동기 스트림(cold,
collect해야 흐름). 연산자map/debounce/flatMapLatest/flowOn/catch. Hot:StateFlow(현재값 1개·UI 상태·초기값 필수·conflate) vsSharedFlow(이벤트·replay 조절). 수집은repeatOnLifecycle(STARTED)로 생명주기 안전,stateIn으로 cold→hot.
(출처: Kotlin Flow / Android — StateFlow/SharedFlow / Collect safely / Kotlin in Action 2e Ch 16.)