cold Flow를 hot StateFlow로 바꿔 화면 상태로 노출한다.
val uiState: StateFlow<HomeUiState> = repository.observeUsers()
.map { HomeUiState(users = it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = HomeUiState(isLoading = true)
)
cold와 hot
-
cold Flow — 구독할 때마다 처음부터 실행된다
- 화면 두 개가 collect 하면 DB 쿼리가 두 번 돈다
-
hot StateFlow — 하나의 실행을 여러 구독자가 공유한다
- 현재 값을 항상 들고 있어 새 구독자가 즉시 받는다
WhileSubscribed(5_000)의 5초
화면을 회전하는 동안 구독자가 잠깐 0 이 된다
-
Activity 파괴 → (구독자 0) → Activity 재생성 → 재구독
-
Eagerly / Lazily — 구독자와 무관하게 계속 돈다 → 백그라운드에서도 낭비
-
WhileSubscribed(0) — 구독이 끊기는 즉시 중단 → 회전마다 DB·네트워크를 다시 탄다
-
WhileSubscribed(5000) 5초 유예 → 회전은 넘기고 진짜 떠난 경우만 정리한다
replayExpiration도 있다
SharingStarted.WhileSubscribed(
stopTimeoutMillis = 5_000,
replayExpirationMillis = Long.MAX_VALUE // 기본값. 마지막 값을 계속 들고 있는다
)
0 으로 주면 중단 시 값을 버리고 initialValue 로 되돌아간다
- 오래 떠났다 돌아왔을 때 옛 데이터 대신 로딩을 보여 주고 싶으면 쓴다
initialValue가 강제되는 이유
StateFlow 는 정의상 항상 값이 있어야 한다
- 업스트림이 아직 아무것도 안 냈을 때 줄 값이 필요하다
보통 Loading 상태나 빈 상태를 준다
여러 Flow를 합쳐 상태를 만든다
val uiState = combine(
repository.observeUsers(),
savedStateHandle.getStateFlow("query", ""),
) { users, query ->
HomeUiState(users = users.filter { it.name.contains(query) }, query = query)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), HomeUiState())
이 형태의 장점
- 상태 조합이 한 곳에 모인다
- MutableStateFlow 를 직접 갱신하는 코드가 사라진다
- 업스트림이 SSOT(DB) 라 어디서 바뀌어도 자동 반영된다
면접 함정
- ❌ "stateIn을 안 쓰고 그냥 Flow를 노출해도 된다" → 구독자마다 업스트림이 새로 돈다. 화면 회전이 곧 재조회다.
- ❌ "Eagerly가 가장 빠르다" → 앱이 백그라운드일 때도 돌아 배터리를 쓴다.
shareIn과의 차이
- stateIn — StateFlow 를 만든다. 값이 항상 있다. replay 1 고정. 중복 값은 걸러진다
- shareIn — SharedFlow 를 만든다. replay 를 정한다. 초기값이 없다. 중복도 방출한다
화면 상태 → stateIn 이벤트 스트림 공유 → shareIn(replay = 0)
업스트림이 재시작되는 조건
WhileSubscribed 는 '구독자 수' 만 본다 구독자 0 → 타임아웃 → 업스트림 취소 다시 구독 → 업스트림 재시작 (DB 쿼리 · 네트워크 다시)
주의: 재시작해도 initialValue 로 돌아가지는 않는다
- (replayExpiration 기본값이 무한이라 마지막 값을 들고 있다)
- 화면에는 옛 값이 잠깐 보이고 새 값으로 갱신된다
에러 처리
val uiState = repository.observeUsers()
.map<List<User>, UiState> { UiState.Success(it) }
.catch { emit(UiState.Error(it.message ?: "알 수 없는 오류")) } // ★ stateIn 앞에
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState.Loading)
catch 가 없으면 업스트림 예외가 스코프를 취소한다
- 그 ViewModel 의 다른 코루틴까지 함께 죽는다
- 화면이 영영 갱신되지 않는다