
stateIn과 shareIn은 Android에서 cold Flow를 UI가 안정적으로 구독할 수 있는 hot stream으로 바꿀 때 자주 만나는 함수입니다.
둘 다 Flow를 공유한다는 점은 비슷하지만, 결과 타입과 의도가 다릅니다. 이 차이를 모르면 ViewModel 코드가 금방 복잡해집니다.

stateIn: 마지막 값이 중요하고 UI가 현재 상태를 즉시 알아야 할 때shareIn: 같은 upstream을 여러 collector가 공유하거나 이벤트 흐름을 나눠 받을 때
상태는 stateIn, 공유 스트림은 shareIn이라고 먼저 생각하면 큰 방향을 잡기 쉽습니다.
cold Flow를 그대로 UI에서 collect하면 생기는 문제
Repository가 반환하는 Flow는 대개 cold Flow입니다. collect가 시작될 때 upstream이 실행되고, collector가 여러 개면 같은 작업이 반복될 수 있습니다.
UI 재구성, 화면 회전, lifecycle 변화가 겹치면 같은 데이터를 다시 구독하거나, 화면에 필요한 초기 상태가 애매해질 수 있습니다.
ViewModel에서 Flow를 상태로 바꾸는 이유는 이 문제를 줄이기 위해서입니다. UI는 ViewModel이 만든 안정적인 상태를 관찰하고, upstream 구독 정책은 ViewModel에서 관리합니다.
stateIn은 현재 값이 필요한 UI 상태에 맞다
stateIn은 Flow를 StateFlow로 바꿉니다. StateFlow는 항상 현재 값을 가진다는 성격이 강합니다.
예를 들어 화면에 보여줄 목록, 로딩 상태, 에러 메시지, 선택된 탭처럼 UI가 언제든 현재 값을 알아야 하는 데이터에 잘 맞습니다.
data class UserUiState(
val isLoading: Boolean = true,
val name: String = "",
val errorMessage: String? = null
)
val uiState: StateFlow<UserUiState> =
repository.userStream()
.map { user -> UserUiState(isLoading = false, name = user.name) }
.catch { emit(UserUiState(isLoading = false, errorMessage = "사용자 정보를 불러오지 못했습니다.")) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UserUiState()
)여기서 중요한 것은 initialValue입니다. UI가 처음 구독하는 순간에도 보여줄 값이 있어야 하므로, 빈 상태나 로딩 상태를 명시합니다.
shareIn은 Flow를 SharedFlow로 바꿉니다. 마지막 값을 상태처럼 들고 있는 것보다, 여러 collector가 같은 upstream을 공유하는 의미가 더 강합니다.
예를 들어 센서 이벤트, 소켓 메시지, 외부 콜백을 Flow로 감싼 스트림처럼 값 자체보다 흐름 공유가 중요한 경우가 있습니다.
val sharedMessages: SharedFlow<Message> =
messageSource.messages()
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
replay = 0
)replay를 0으로 두면 새 collector에게 과거 값을 다시 주지 않습니다. 반대로 최근 이벤트를 일부 다시 줘야 한다면 replay 값을 조정할 수 있습니다.
SharingStarted를 대충 고르면 안 된다
stateIn과 shareIn 모두 SharingStarted 정책을 받습니다. 이 값은 upstream을 언제 시작하고 언제 멈출지 정합니다.
Eagerly: collector가 없어도 바로 시작Lazily: 첫 collector가 나타날 때 시작WhileSubscribed: 구독자가 있을 때만 유지하고, 사라지면 일정 시간 뒤 중지 가능
Android ViewModel에서는 화면이 잠깐 사라졌다 돌아오는 상황이 많습니다. 그래서 WhileSubscribed(5_000)처럼 짧은 유예 시간을 두는 패턴이 자주 쓰입니다.
UI에서는 lifecycle-aware 수집을 함께 써야 한다
ViewModel에서 StateFlow를 만들었다고 끝이 아닙니다. UI가 lifecycle과 무관하게 계속 collect하면 화면이 보이지 않을 때도 불필요한 작업이 이어질 수 있습니다.
Compose에서는 collectAsStateWithLifecycle을 우선 고려하고, XML 기반 UI에서는 lifecycle repeatOnLifecycle 패턴을 검토하는 편이 안전합니다.
정리
출처 확인: Android Developers의 StateFlow와 SharedFlow 문서, Kotlin coroutines의 stateIn 문서와 shareIn 문서를 기준으로 정리했습니다.
stateIn과 shareIn은 둘 다 Flow를 공유하기 위한 도구지만, 선택 기준은 다릅니다. UI가 현재 값을 필요로 하면 stateIn, 같은 흐름을 공유하거나 이벤트 스트림을 다루면 shareIn을 먼저 떠올리면 됩니다.
이전 글인 callbackFlow는 언제 써야 할까와 함께 보면, 콜백을 Flow로 바꾼 뒤 ViewModel에서 UI 상태로 연결하는 흐름까지 이어서 이해할 수 있습니다.