State는 객체 지향 관점에서 자주 사용되는 단어로 객체가 특정 시점에서 어떤 데이터 값을 가지고 있는지 나타내는 것이다.
안드로이드의 UI 레이어 가이드에서는 UI 레이어의 UI State를 생성하고 관리하는 수단으로 단방향 데이터 흐름(UDF)을 설명한다.
이러한 State는 UI와 관련된 데이터를 저장하고 관리하는데 사용되는 상태 홀더인 ViewModel을 통해 관리된다.
상태 홀더인 ViewModel은 앱이 상태를 읽을 수 있도록 상태를 저장한다.
로직이 필요한 경우 필요한 로직을 호스팅하는 데이터 소스에 대한 액세스 권한을 제공한다.
이러한 State 데이터를 다루기 위해 안드로이드에서는 LiveData를 사용했었다.
다만 LiveData는 액티비티의 LifeCycle에 반응해 데이터의 변경 사항을 관찰하고 이에 대응한다.
따라서 만약 액티비티가 있는 UI 레이어
가 아닌 Domain 레이어
와 상호작용한다면 기존의 LiveData를 사용할 수가 없어진다는 단점이 있다.
이럴 때 사용할 수 있는 API가 바로 Flow이다.
Cold Stream은 Flow
를 수집하는 각 Collector
가 독립적인 데이터 스트림을 생성하여 개별적으로 데이터를 수집한다. 즉, Collector
마다 새로운 데이터 흐름이 시작되며, 데이터를 수집(collect
)하지 않으면 아무 작업도 수행되지 않는다.
ex) 사용자가 버튼을 눌러 데이터를 새로고침할 때마다 새로운 네트워크 요청을 보내는 경우.
반면, Hot Stream은 여러 Collector
가 동일한 데이터 스트림을 공유하여 같은 데이터를 수집한다. 일부 구현에서는 Collector
가 있을 때만 시작되지만, 기본적으로 Collector
가 없어도 데이터 제공자(Provider)는 계속해서 데이터를 생성하고 스트림을 유지한다.
ex) UI 상태 관리 (예: Dark Mode 토글, 로그인 상태 등)
Flow는 우편함에서 직접 편지를 가져오는 방식과 같다.
우체부가 새로운 편지를 보냈다고 해도, 내가 우편함을 열어보지 않으면(collect 하지 않으면) 절대 편지를 받을 수 없다.
또, 우편함을 열 때마다 처음부터 다시 새롭게 배달된다.
💡 Flow의 특징
✔️ 데이터를 하나씩 순차적으로 받는 구조 (ex: API 요청)
✔️ Cold Stream → 직접 요청해야만 데이터 수신 가능 (collect
필요)
✔️ 값을 유지하지 않음 → 현재 상태(state) 개념이 없음
fun getMessages(): Flow<String> = flow {
val messages = listOf("첫 번째 편지", "두 번째 편지", "세 번째 편지")
for (msg in messages) {
delay(100)
emit(msg) // 데이터를 방출
}
}
fun main() = runBlocking {
getMessages().collect { println(it) }
// 직접 우편함을 열어야(collect) 편지를 받을 수 있음
}
collect
) "첫 번째 편지", "두 번째 편지", "세 번째 편지" 순서대로 받음 StateFlow는 항상 최신 정보를 표시하는 전광판과 같다.
전광판은 항상 최신 정보를 유지하고 있어서 새로 온 사람도 현재 상태를 바로 볼 수 있다.
💡 StateFlow의 특징
✔️ 현재 상태(state)를 항상 유지 (초기값 필요)
✔️ Hot Stream → 구독하지 않아도 최신 값 유지
✔️ .value 사용 가능 → 언제든지 현재 상태 확인 가능
class MyViewModel : ViewModel() {
private val _stateFlow = MutableStateFlow("기본 상태") // 전광판에 처음 표시될 내용
val stateFlow = _stateFlow.asStateFlow()
fun updateState(newText: String) {
_stateFlow.value = newText // 전광판의 내용을 변경
}
}
lifecycleScope.launchWhenStarted {
myViewModel.stateFlow.collectLatest {
binding.textView.text = it // 전광판이 바뀌면 UI도 자동으로 변경됨
}
}
→ collectLatest
를 사용하면 최신 상태만 갱신됨
SharedFlow는 라디오 방송처럼 특정 순간에만 송출되는 데이터 스트림이다.
라디오를 켜면(collect 하면) 지금 방송 중인 내용을 들을 수 있지만,
라디오를 늦게 켜면 이미 지난 방송은 들을 수 없다.
💡 SharedFlow의 특징
✔️ 여러 구독자에게 동시에 데이터 전송 가능
✔️ 이전 값 유지 X → .value
불가능 (이벤트성 데이터에 적합)
✔️ 버퍼 설정 가능 → 필요하면 일정 시간 동안 데이터를 보관 가능
class MyViewModel : ViewModel() {
private val _snackbarFlow = MutableSharedFlow<String>()
val snackbarFlow = _snackbarFlow.asSharedFlow()
fun showSnackbar(message: String) {
viewModelScope.launch {
_snackbarFlow.emit(message) // Snackbar 메시지 이벤트 발생
}
}
}
---------------------------------------------
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel(), scaffoldState: ScaffoldState) {
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
viewModel.snackbarFlow.collect { message ->
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(message) // Snackbar 표시
}
}
}
Scaffold(
scaffoldState = scaffoldState,
content = {
Button(onClick = { viewModel.showSnackbar("버튼이 눌렸어요!") }) {
Text("Snackbar 띄우기")
}
}
)
}
개념 | 예시 | Cold/Hot | 현재 값 유지 | 사용 예 |
---|---|---|---|---|
Flow | 우편함 | Cold | ❌ 없음 | 리스트 가져오기 |
StateFlow | 전광판 | Hot | ✅ 있음 | UI 상태 유지 |
SharedFlow | 라디오 방송 송출 | Hot | ❌ 없음 | 이벤트 발생 |
단순히 데이터를 가져와서 UI에 표시하고 싶다면?
✅ Flow
현재 상태를 기억하고 싶다면?
✅ StateFlow
이벤트(알림, 메시지) 처리에 사용하고 싶다면?
✅ SharedFlow
StateFlow
(읽기 전용)class MyViewModel : ViewModel() {
private val _stateFlow = MutableStateFlow(0) // 내부에서 상태를 변경 가능
val stateFlow = _stateFlow.asStateFlow() // 외부에서는 읽기 전용 (값 수정 불가)
fun increment() {
_stateFlow.value += 1 // 내부에서만 값 변경
}
}
stateFlow
는 외부에서 읽기만 가능하다. 값을 가져오지만 변경할 수 없다. 따라서 외부에서는 단지 현재 값을 관찰하거나 collect
할 수만 있다.MutableStateFlow
(읽기/쓰기 가능)class MyViewModel : ViewModel() {
val mutableStateFlow = MutableStateFlow(0) // 읽기와 쓰기 모두 가능
fun increment() {
mutableStateFlow.value += 1 // 값을 직접 변경 가능
}
}
mutableStateFlow
는 외부에서 읽고 쓰기가 가능하다.collect
를 통해 변경된 값을 감지할 수 있다. 외부에서는 값을 변경할 수 없고, 내부에서만 수정 가능하다.따라서, StateFlow는 외부에 공개된 상태를 나타내는 용도로 사용하고,
MutableStateFlow는 상태를 관리하고 변경하는 용도로 사용한다.
MainActivity에서 StateFlow의 상태를 변화시키고 이를 collect 한 다음 SnackBar를 출력하는 코드가 있다고 가정하겠다.
버튼을 클릭하면 스낵바를 출력할 수 있다.
하지만 버튼을 다시 클릭한다고 해도 스낵바는 생성되지 않는다. 상태가 변하지 않기 때문이다.
그런데 화면이 회전하는 등의 행위로 액티비티가 재생성 된다면, StateFlow는 자동으로 value를 다시 emit 해주게 된다. 그래서 스낵바가 생성된다.
이러한 StateFlow의 특성은 스낵바와 같은 일회성 작업을 하고 싶을 때는 적합하지 않다.
즉, StateFlow는 새로운 값이 들어와야만 UI가 갱신되기 때문에,
Snackbar를 띄우는 stateFlow.collectLatest { }
코드는 값이 변할 때만 실행된다.
같은 값을 다시 emit하면 UI가 갱신되지 않는다!
SharedFlow는 어떨까?
SharedFlow는 상태가 아닌 이벤트를 전달하기에 버튼을 몇번이나 누르던 스낵바를 생성한다. 그리고 액티비티가 재생성 될때에는 스낵바를 생성하지 않는다.
SharedFlow는 과거의 데이터를 저장하지 않기 때문이다.
즉, 이전 이벤트를 기억하지 않기 때문에 화면이 회전해도 새롭게 emit되지 않는다.
결과적으로 Snackbar가 다시 나타나지 않는 것이다.
이는 SharedFlow가 One-Time 이벤트에 매우 적합하다는 것을 의미한다.
StateFlow | SharedFlow | |
---|---|---|
핵심 개념 | 최신 상태 유지 (State) | 새로운 이벤트 전송 (Event) |
Snackbar 실행 | 값이 변할 때만 | 이벤트 발생할 때마다 |
같은 값 emit 시 UI 변화 | X (변화 없음) | O (항상 실행됨) |
화면 회전 후 동작 | 기존 값 다시 emit → Snackbar 뜸 | 기존 이벤트 기억 X → Snackbar 안 뜸 |
주요 사용 예시 | UI 상태 유지 (ex. 버튼 활성화 여부) | One-time Event (ex. 메시지 알림) |
출처
https://coding-juuwon2.tistory.com/349
https://developer.android.com/kotlin/flow?hl=ko#modify
https://everyday-develop-myself.tistory.com/306