지난번에는 Flow의 개념과 간단한 사용법에 대해 알아봤다. 그런데 LiveData와 쓰임이 똑같은 것 같다는 생각이 들었다. 오늘은 LiveData와 Flow의 차이를 알아보고, LiveData의 장점을 반영한 State Flow, Shared Flow에 대해 공부해보자.
우선 LiveData는 안드로이드 Lifecycle을 인식하는, 관찰 가능한 데이터 홀더 클래스다.
안드로이드 Lifecycle을 인식하기 때문에 메모리 누수면에서 뛰어나고, 수명주기를 따로 처리해주지 않아도 되며, 중지된 활동으로 인한 비정상적 종료가 없다는 대표적인 장점이 있다.
이에 반해 Flow는 Lifecycle을 인식하지 못한다. 그렇다면 LiveData가 더 좋아보이는데 왜 요즘 안드로이드 진영에서는 Flow가 LiveData를 대체하고 있는 걸까?
안드로이드 공식문서에서 LiveData의 단점을 알려주고있다.
데이터 영역 클래스에서 LiveData 객체를 작업하고 싶을 수 있지만 LiveData는 비동기 데이터 스트림을 처리하도록 설계되지 않았습니다. LiveData 변환과 MediatorLiveData를 사용하여 LiveData 객체 작업을 할 수는 있지만 이 접근 방식에는 단점이 있습니다. 즉, 데이터 스트림을 결합하는 기능이 매우 제한적이고 변환을 통해 만들어진 객체를 포함하여 모든 LiveData 객체가 기본 스레드에서 관찰됩니다.
즉 Presentation Layer가 아닌 Layer에서 LiveData를 중간연산하려면 Main Thread를 차단하여, 무거운 연산이 실행될 경우 ANR이 발생할 수 있다.
안드로이드는 이를 Rx와 같은 비동기 처리 라이브러리를 사용해 해결했지만 Coroutine과 Flow의 등장으로 이를 대체하게 된 것이다.
또한 클린 아키텍처 관점에서 봤을 때 순수 Java/Kotlin 코드로만 구성되어야 하는 Domain Layer에서 안드로이드 플랫폼에 종속적인 LiveData를 사용하는 것은 위반되는 사항이다.
아쉽게도 Flow에도 문제가 있다.
정리해보자면 LiveData는 Presentation Layer가 아닌 Layer에서 사용할 때 비동기 처리적으로 문제가 있고, Flow는 Lifecycle을 인식하지 못하며 Presentation Layer의 여러개의 소비자가 collect할 때 비효율적이라는 문제점이 있다.
그렇기때문에 Flow를 통해 비동기적으로 데이터 스트림을 생성하고 연산한 뒤, ViewModel과 같은 Presentation Layer에서 .asLiveData()를 통해 LiveData로 변환 후 View에서 사용하는 방법도 권장한다.
위와 같은 Flow의 단점을 해결하기 위해 나온 것이 State Flow와 Shared Flow다. 공식문서에 설명이 나와있다.
StateFlow는 현재 상태와 새로운 상태 업데이트를 수집기에 내보내는 관찰 가능한 상태 홀더 흐름입니다. value 속성을 통해서도 현재 상태 값을 읽을 수 있습니다.
쉽게 말하자면 데이터 홀더와 데이터 스트림의 역할을 모두 하는 것이 State Flow라는 것이다. (즉 여러개의 소비자가 collect를 하더라도 매번 데이터를 요청해 새로운 flow를 만드는 것이 아니라 데이터 홀더역할도 하는 state flow를 구독하면 n회의 데이터 발행이 필요 없어지지 않을까...? 이 부분은 내가 이해한 부분을 서술한 것으로 정답이 아닐 수 있다.)
또한 State Flow는 Hot Stream 방식으로 소비자의 collect 요청이 없어도 바로 값을 내보내고, 데이터가 없데이트 될 때마다 데이터를 발행한다.
그리고 Flow와 다르게 .value로 현재 값을 읽어올 수 있다.
Live Data와 State Flow는 많은 면에서 닮아 있지만 다르게 사용해야 하는 부분이 있다.
다음은 flow를 state flow로 바꿔주는 stateIn 함수이다.
fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T
): StateFlow<T>
위에서 볼 수 있듯 3개의 파라미터가 존재한다.
flow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
0
)
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}
안드로이드 공식문서에서 발췌한 소스코드다.
Live Data와 같이 MutableStateFlow에 .value를 사용해 최신 데이터를 저장하며 이를 StateFlow에 할당하고, View에서는 이 StateFlow를 구독하는 형태를 갖는다.
이제 Live Data를 State Flow로 대체할 수 있을 것 같다. 여기서 살펴보지 않은 Shared Flow도 있다. State Flow가 Shared Flow를 상속받았기 때문에 전반적으로 비슷하지만, 초기값을 갖지 않는다던가 하는 몇몇 차이점이 존재한다. State Flow 만으로 해결이 쉽지 않는 문제를 만났을 때 Shared Flow에 대해서도 공부 해봐야겠다.
https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ko
https://kotlinworld.com/233?category=973477