안드로이드 시스템·Compose 용어 사전
코루틴WhileSubscribed · SharingStarted

stateIn

cold Flow를 hot StateFlow로 바꿔 화면 상태로 노출하는 연산자.

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 의 다른 코루틴까지 함께 죽는다
  • 화면이 영영 갱신되지 않는다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 코루틴 on Android — lifecycleScope·repeatOnLifecycle