안드로이드 시스템·Compose 용어 사전
상태LiveData · MutableStateFlow

StateFlow

항상 값을 가지는 코루틴 기반 상태 스트림. LiveData를 대체한다.

항상 값을 가지는 코루틴 기반 상태 스트림.

LiveData와의 차이

                 LiveData                 StateFlow
초기값           없어도 된다 (null 가능)   반드시 있다
생명주기 인지    내장 (observe 하나로)     없다 → repeatOnLifecycle 필요
연산자           거의 없다 (map/switchMap) Flow 전체 (combine·debounce·flatMapLatest)
스레드           메인에서만 setValue      아무 데서나
플랫폼           안드로이드 전용           순수 코틀린 (테스트·KMP 유리)
중복 값          그대로 전달              같은 값이면 방출하지 않는다 (conflate)
// StateFlow — 값이 항상 있다
private val _state = MutableStateFlow(HomeUiState())
val state: StateFlow<HomeUiState> = _state.asStateFlow()

_state.update { it.copy(isLoading = true) }   // 원자적. value = 보다 안전하다

왜 update를 쓰나

// ❌ 읽고-고치고-쓰기 사이에 다른 코루틴이 끼어들 수 있다
_state.value = _state.value.copy(count = _state.value.count + 1)

// ✅ compare-and-set 루프로 원자성을 보장한다
_state.update { it.copy(count = it.count + 1) }

SharedFlow와의 구분

  • StateFlow — 현재 상태. 최신 값 1개를 replay 한다

    • 새 구독자가 즉시 현재 값을 받는다
    • 같은 값 재방출은 무시된다
  • SharedFlow — 이벤트. replay 를 직접 정한다 (기본 0)

    • 같은 값도 그대로 방출된다
  • "토스트를 StateFlow 로 보내면 회전할 때 다시 뜬다" 가 이 차이 때문이다

수집은 반드시 생명주기 안전하게

// Compose
val state by viewModel.uiState.collectAsStateWithLifecycle()

// View 시스템
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { render(it) }
    }
}

StateFlow 는 생명주기를 모른다

  • 그냥 collect 하면 화면이 백그라운드에 가도 계속 수집한다
  • 보이지도 않는 화면을 위해 데이터를 받고 다시 그린다

LiveData 에서 옮겨올 때 가장 많이 빠뜨리는 지점이다

면접 함정

  • "StateFlow가 LiveData의 상위 호환" → 생명주기 인지가 빠져 있어 수집 코드가 늘어난다. 그 대신 연산자와 플랫폼 독립성을 얻는다.
  • "data class면 중복 방출이 자동으로 걸러진다"equals 기준이다. 배열 필드가 있으면 항상 다르다고 판정된다.

LiveData에서 옮길 때의 대응표

  • MutableLiveData(x) — → MutableStateFlow(x)
  • liveData.value = x — → _flow.value = x (또는 _flow.update { })
  • postValue(x) — → 필요 없다 (아무 스레드에서 안전)
  • map { } — → map { } (Flow 연산자)
  • switchMap { } — → flatMapLatest { }
  • MediatorLiveData — → combine { }
  • observe(owner) { } — → repeatOnLifecycle + collect

asStateFlow를 쓰는 이유

// ❌ 밖에서 캐스팅해 값을 바꿀 수 있다
val state: StateFlow<UiState> = _state
(state as MutableStateFlow).value = ...

// ✅ 읽기 전용 래퍼
val state: StateFlow<UiState> = _state.asStateFlow()

중복 방출이 걸러지는 것의 함정

data class UiState(val items: List<Item>)

// items 리스트의 '내용' 이 바뀌었지만 같은 인스턴스라면
_state.value = _state.value.copy(items = sameListInstance)
// → equals 가 true → 방출되지 않는다 → 화면이 안 바뀐다
  • 가변 컬렉션을 상태에 담고 그것을 제자리 수정(mutate) 하면 이 문제가 난다
  • 상태 안의 컬렉션은 반드시 불변으로 다루고 새 리스트를 만든다

테스트

@Test fun 새로고침() = runTest {
    vm.uiState.test {                        // app.cash.turbine
        assertTrue(awaitItem().isLoading)
        vm.refresh()
        assertEquals(3, awaitItem().users.size)
        cancelAndIgnoreRemainingEvents()
    }
}

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — ViewModel·LiveData·StateFlow — 생명주기 안전 수집