기존에 개발했던 메이플스토리 검색앱에서 사용하던 LiveData를 Flow로 변환하면서 알게된 내용입니다. 틀린 부분이 있으면 알려주세요!
처음 MVVM구조를 만들면서 ViewModel을 만들고 그거를 이제 observe하면서 데이터가 바뀐 것을 읽는 형식으로 만들었습니다.
LiveData의 장점
보통 이런식으로 계층구조를 만들어서 사용을 하는데 LiveData 같은 경우 Domain Layer에서 사용하는걸 추천하지 않습니다. 보통은 Flow로 하는걸 추천을 하더라구요. 이유는 Domain Layer는 Java와 Kotlin 코드로만 구성하는 것이 좋으며 LiveData를 사용하게 되면 그 순간부터 Android에 의존적일수 있기에 단위테스트를 따로해야하는 경우가 생길 수 있습니다.
구글 공식 문서에서도 Flow로 만든 다음 asLiveData()로 변환해서 사용하는걸로 작성이 되어있습니다.
데이터 영역 클래스에서 LiveData 객체를 작업하고 싶을 수 있지만 LiveData는 비동기 데이터 스트림을 처리하도록 설계되지 않았습니다. LiveData 변환과 MediatorLiveData를 사용하여 LiveData 객체 작업을 할 수는 있지만 이 접근 방식에는 단점이 있습니다. 즉, 데이터 스트림을 결합하는 기능이 매우 제한적이고 변환을 통해 만들어진 객체를 포함하여 모든 LiveData 객체가 기본 스레드에서 관찰됩니다.
간단히 말하면 LiveData는 비동기에서 쓰기에 적절하지 않다.
그래서 저는 제가 개발했던 앱을 LiveData로 되어있던 Domain Layer 계층을 Flow로 바꿀 생각을 하고 있었습니다. 그래서 Coroutine에 대해 알아보고 Flow에 대해 알아보던 중 StateFlow에 대해 알게 되었습니다. 그 전에 Flow에 대해 간단하게 알아보고 가겠습니다.
Flow는 말 그대로 데이터의 흐름들 속에서 데이터를 처리, 변환 할 수 있으며 비동기 데이터 스트림을 처리할 수 있습니다.
기본적으로 데이터를 생산(emit)하고 소비(collect)하는데 사용됩니다.
Flow는 기본적으로 Cold Stream 입니다.
이러한 문제점이 있었습니다. 그래서 Flow로 받아서 asLiveData()로 변환해서 LiveData로 사용하고 있었습니다.
StateFlow는 SharedFlow 의 한 종류이며 Hot Stream의 성격을 가지고 있습니다.
사용하다보면 LiveData와 비슷하다고 느꼈는데 가장 큰 차이점은 StateFlow는 기본 값을 가지고 있다인 것 같습니다.
이러한 특징들로 인해 LiveData를 대체할 수 있습니다.
제가 사용했던 코드를 보면서 마무리 하겠습니다.
MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {
//기본값을 넣어줘야한다.
private val _userData = MutableStateFlow(ResultVO())
val userData: StateFlow<ResultVO> get() = _userData
private val _errorLiveData = MutableStateFlow("")
val errorLiveData: StateFlow<String> get() = _errorLiveData
fun clearErrorData() {
_errorLiveData.value = ""
}
fun getUserData(id: String) {
viewModelScope.launch {
try {
//가져온 데이터를 발행(emit)
_userData.emit(userRepository.getUserData(id))
clearErrorData()
} catch (e: Exception) {
_errorLiveData.emit(e.message.toString())
}
}
}
}
MainView.kt
@Composable
fun ParallaxEffect(
navigateBack: () -> Unit,
id: String?,
) {
...
val viewModel: MainViewModel = hiltViewModel()
val likeCharacterViewModel: LikeCharacterViewModel = hiltViewModel()
val recentSearchViewModel: RecentSearchViewModel = hiltViewModel()
val userData by viewModel.userData.collectAsState()
val errorData by viewModel.errorLiveData.collectAsState()
...
if (errorData.isNotEmpty()) {
SearchFailedDialog(
errorData = errorData.toString(),
onDismiss = { viewModel.clearErrorData() })
}
MainAvatar(userData = userData)
}
사용방법은 LiveData와 거의 유사합니다.
참고
https://developer.android.com/topic/architecture?hl=ko
https://medium.com/@apfhdznzl/flow%EC%99%80-channel-cold-stream%EA%B3%BC-hot-stream-c42c64cf4996
https://www.youtube.com/watch?v=6z3QjG7vLkE&ab_channel=%EA%B0%9C%EB%B0%9C%ED%95%98%EB%8A%94%EC%A0%95%EB%8C%80%EB%A6%AC