Android Jetpack 라이브러리 중 하나인 LiveData
는 관찰 가능한 데이터 홀드 클래스로 앱의 UI 상태를 업데이트하는 데 많이 써 왔다. 하지만, 최근 코루틴의 Flow API 중 하나로 StateFlow
가 등장하여 LiveData
를 대체하고 있다.
이 글에서는 어떻게 LiveData
를 StateFlow
로 대체할 수 있는지 알아보도록 하겠다.
LiveData는 Activity
, Fragment
, Service
등 안드로이드 앱 컴포넌트의 생명 주기를 인식하여 메모리 누수 없이 데이터 관리를 해준다는 장점이 있다.
그러나 LiveData에는 다음과 같은 한계가 있다.
비동기 스트림을 지원하지 않는다.
LiveData
는 UI와 밀접하게 연관되어 있어 오직 메인스레드(Main Thread)에서만 읽고 쓸 수 있다. 따라서 ViewModel
에서 LiveData
를 사용하여 View를 업데이트할 때는 사용할 수 있지만, Data Layer에서 데이터를 처리할 때는 사용하기 어렵다. 데이터를 I/O 할 때에는 메인스레드(Main Thread)가 아닌 작업스레드(Worker Thread)에서 비동기 방식으로 처리되어야 하기 때문이다.
안드로이드 플랫폼에 종속적이다.
LiveData
가 안드로이드 생명주기를 인식한다는 점은, Clean Architecture 관점에서 보았을 때 단점이 될 수도 있다. Presentation Layer에서는 편리하게 사용할 수 있지만, 플랫폼에 종속적이지 않은 순수한 Kotlin 및 Java 코드로 이루어져야 하는 Domain Layer(Business Layer)에서는 사용하기 어렵기 때문이다.
위와 같은 LiveData
의 한계를 극복하여 대신 사용할 수 있는 것이 Flow
이다.
Flow
는 값을 순차적으로 방출(emit)하고 정상적 혹은 예외적으로 완료되는 비동기 데이터 스트림이기 때문에 Data Layer에서 쓸 수 있다. 또한, 코틀린의 코루틴 API이기 때문에 안드로이드 플랫폼에 종속적이지도 않다.
다만, ViewModel에서는 Flow
를 직접 쓰는 것이 아니라 StateFlow
를 사용하고, View에서는 Lifecycle.repeatOnLifecycle
블록에서 이를 수집하여 사용하여 LiveData
처럼 안드로이드의 생명주기를 직접 인식할 수 없는 문제를 해결한다.
StateFlow
는 Flow API 중 하나로 현재 상태와 새로운 상태 업데이트를 collector에 내보내는 관찰 가능한 State Holder Flow이다. LiveData
와 마찬가지로 value 속성을 통해 현재 상태 값을 읽을 수 있다.
StateFlow의 특징은 다음과 같다.
Hot stream이기 때문에 생성하자마자 바로 활성화되며, 값이 업데이트 된 경우에만 반환한다.
항상 한 개의 값을 가지고 있기 때문에 초기 상태를 생성자에게 전달해야 한다.
여러 개의 collector 를 지원하기 때문에 중복 리소스 요청을 방지한다.
collector 수에 관계없이 항상 구독하고 있는 것의 최신 값을 받는다.
그렇다면 간단한 노트 앱을 만든다고 가정하여 실제 코드를 살펴보자.
NotesViewModel에서는 다음 두 가지 일을 하고 있다.
loadData
)openDetails
)첫 번째 경우는 뷰의 상태를 처리한다. 따라서 화면 구성이 변경되었을 때도 마지막으로 방출된 노트 목록 값이 수신되어 보여져야 한다. 두 번째 경우는 화면이 이동되는 일회성 이벤트로 값을 오직 한 번만 받는다.
LiveData를 활용하면 다음과 같이 구현할 수 있다.
class NotesViewModel(
private val noteRepository: NoteRepository
) : ViewModel() {
private val _uiState = MutableLiveData<UiState<List<Note>>>()
val uiState: LiveData<UiState<List<Note>>> = _uiState
private val _navigationEvent = SingleLiveEvent<NavigationEvent>()
val navigationEvent: SingleLiveEvent<NavigationEvent> = _navigationEvent
fun loadData() {
_uiState.value = UiState.Loading
viewModelScope.launch {
noteRepository.getAllNotes().collect {
_uiState.value = UiState.Success(it)
}
}
}
fun openDetails() {
_navigationEvent.value = NavigationEvent.ToDetails
}
}
우선 loadData()
메소드를 살펴보면, NoteRepository
에서 반환한 Flow 객체를 collect
하여 MutableLiveData인 _uiState
객체에 value
를 넣어주고 있다.
그리고 뷰에서 노트 아이템 하나를 클릭했을 때 openDetails()
가 호출된다. 이 때 상세 화면으로 이동하는 이벤트가 뷰로 전달되어야 한다. 그런데 MutableLiveData
를 쓰면 명시적으로 발행된 이벤트만 전달되는 것이 아니라 이전에 발행되었던 이벤트까지 전달될 수 있기 때문에 MutableLiveData
를 확장하여 만든 SingleLiveEvent 클래스를 만들어 화면 이동 처리를 해준다.
class NotesViewModel(
private val noteRepository: NoteRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<Note>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Note>>> = _uiState.asStateFlow()
private val _navigationEvent = MutableSharedFlow<NavigationEvent>(replay = 0)
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent
fun loadData() {
viewModelScope.launch {
noteRepository.getAllNotes().collect {
_uiState.emit(UiState.Success(it))
}
}
}
fun openDetails() {
viewModelScope.launch {
_navigationEvent.emit(NavigationEvent.ToDetails)
}
}
}
LiveData
와 달리 StateFlow
에는 초기 값이 필요하다. 따라서 _uiState
객체를 생성할 때 UiState.Loading
을 초기값으로 한다. 또한 flow 빌더에서 emit()
함수를 통해 NoteRepository
에서 수집한 데이터를 방출하고 있다.
또한 이벤트 처리의 경우 SingleLiveEvent
클래스를 따로 만들어 줄 필요 없이 SharedFlow
를 사용하여 손쉽게 구현할 수 있다. SharedFlow
는 StateFlow
와 달리 초기값을 가지지 않고, replay
매개변수를 통해 값을 수집할 수 있는 횟수를 결정할 수 있다. 여기에서는 replay=0
으로 설정하여 클릭이벤트가 발생했을 때 데이터가 한 번만 수집되도록 하였다. 따라서 화면 회전 등 앱의 구성이 변경해도 이벤트는 다시 발생하지 않는다.
class NotesFragment : Fragment(R.layout.fragment_notes) {
private val notesViewModel: NotesViewModel by viewModels()
private fun observeData() {
notesViewModel.uiState.observe(viewLifecycleOwner) {
handleUiState(it)
}
notesViewModel.navigationEvent.observe(viewLifecycleOwner) {
handleEvent(it)
}
}
}
NotesFragment
에서는 observe
를 통해 ViewModel의 LiveData
를 관찰하여 UI를 업데이트 해준다.
class NotesFragment : Fragment(R.layout.fragment_notes) {
private val notesViewModel: NotesViewModel by viewModels()
private fun observeData() {
viewLifecycleOwner.lifecycleScope.launch {
notesViewModel.uiState.collect {
handleUiState(it)
}
}
viewLifecycleOwner.lifecycleScope.launch {
notesViewModel.navigationEvent.collect {
handleEvent(it)
}
}
}
}
뷰의 생명주기를 인식하는 LiveData
와 달리 Flow
는 생명주기를 알지 못한다. 따라서 뷰가 STOPPED 상태가 되면 LiveData.observe()
는 소비자 등록을 자동으로 취소하는 반면, StateFlow
는 수집을 자동으로 중지하지 않는다. 따라서 lifeCycleScope
확장함수를 통해 생명주기에 따라 변경된 값을 collect
하여 UI를 업데이트 해줄 수 있도록 한다.
SharedFlow
의 경우도 마찬가지로 collect
로 방출된 결과를 받아온다.
지금까지 LiveData의 한계와 StateFlow의 특징을 살펴보고 예제를 통해 LiveData를 StateFlow로 대체하는 방법을 살펴 보았다. 둘 모두 관찰 가능한 데이터 홀더 클래스이며, MVVM 아키텍처 패턴에서 비슷하게 사용할 수 있기 때문에 어렵지 않게 프로젝트에 StateFlow를 적용할 수 있을 것이다.
https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ko
https://alexzh.com/migrate-from-livedata-to-stateflow-and-sharedflow/
https://yjyoon-dev.github.io/android/2022/02/12/android-02/