-
안드로이드 Coroutine Flow - 3 (StateFlow)안드로이드 학습/Android 기술면접 대비 2024. 11. 28. 13:54
안드로이드 앱 아키텍처 예제를 보면 Coroutine과 Flow 뿐만 아니라 ViewModel에서 StateFlow나 SharedFlow를 사용하는 것을 보았다.
MVVM 패턴에서 ViewModel 내부에서 자주 사용했던 것은 LiveData였기 때문에 StateFlow는 무엇이 다른지 학습해볼 필요성이 있어 추가적으로 학습을 해봤다.
Flow와 비교해보기 위해 우선 LiveData에 대해 간략하게 알고 넘어가 보자
LiveData :
LiveData 같은 경우 안드로이드의(Activity나 Fragment의 LifeCycler을 인식하는 데이터 Holder 클래스다. 그래서 LiveData는 생명주기 관리가 따로 필요하지 않은 편리함 때문에 MVVM 아키텍처를 사용하면서 ViewModel 내에서 자주 사용되었다.
하지만 단점도 명확하다. LiveData는 단일 값의 변경에 초점을 맞추고 있다. 말그대로 데이터를 가지고(Hold) 하는 역할을 한다.
- 비동기 데이터 스트림을 처리하도록 설계되지 않았다.
- 데이터 스트림을 결합하는 기능이 매우 제한적이고 모든 LiveData 객체가 기본 스레드에서 관찰된다.
- (이 이유 때문에 여러 예제들을 보면 RxJava를 Domain Layer와 DataLayer에서 사용하고, UI Layer로 넘어왔을 때는 LiveData로 넘겨주는 방식 사용되는 것을 보았다)
UI Layer가 아닌 Layer에서 Main Thread를 오랫동안 차단할 task를 하면 안되고 또한 클린 아키텍처 관점에서 봤을때 Java/Kotlin 코드로 구성된 Domain Layer에서 안드로이드 플렛폼에 종속적인 LiveData를 사용하는 것이 바람직하지 않다.
Flow
Flow 또한 단점이 있다. 장점은 이미 전에 글들에서 언급되어 있어서 단점만 간단히 살펴보면
- Android lifecycle 인식 불가 : (추가적인 라이프사이클 처리가 필요하다)
- Cold Stream 방식
- Flow는 데이터 소비자가 collect하기 전까지 데이터를 발행하지 않는다. 하나의 Flow 빌더에 여러개의 소비자가 collect를 요청하면 하나의 collect 마다 데이터를 호출하기 때문에 비용적으로 비효율적.
- 데이터 저장 불가
이것 저것 공부해봐서 내 나름대로 종합해본 결과로는, LiveData는 데이터 보관용(?)에 초점이 맞춰져 있고 Flow는 데이터 스트림을 처리하는데 초점이 맞춰져 있기때문에 Flow를 사용하면서 같이 사용하는 StateFlow나 SharedFlow와 좀더 비교하는 것이 좀더 타당하다고 보인다.
종합해보면, Data Layer와 Domain Layer 부분 Coroutine Flow나 RxJava 를 사용하고 UI Layer 부분에서는 LiveData나 StateFlow같은 데이터 holder 하는 클래스를 사용하는 것이 좋아보인다.
한가지 궁금한 점은 Flow LiveData가 각자 장점이 다르기 때문에 Data Layer와 Domain Layer에서는 Flow를 사용하고 UI Layer에서는 LiveData를 사용해도 되는 것이 아닌가 싶었는데 대부분 Flow와 StateFlow를 조합해서 사용했다.
이러한 이유를 시원하게 얘기해는 자료를 찾지 못해서 나중에 좀더 학습해보고 StateFlow를 Flow와 사용하는 이유를 더 알아보겠다.
대략 아래의 조합으로 사용하면 비슷하지 않을까 싶다.
- RxJava (데이터 처리) + LiveData (데이터 관리)
- Flow(데이터 처리) + StateFlow / SharedFlow(데이터 관리)
StateFlow와 SharedFlow
StateFlow
1) StateFlow와 LiveData 차이점 :
- StateFlow의 경우 초기 생성자를 전달해야 하지만 LiveData는 그렇지 않다.
- View가 STOPPED 상태이면 LiveData는 자동으로 등록이 취소 되지만 StateFlow 같은 경우 자동으로 수집이 중지 되지 않는다. 그래서 Lifecycle.repeatOnLifecycle 블록에서 collect 해야한다.
- Flow 자체는 데이터를 갖고 있지 않기 때문에 필요할 때마다 collect를 사용해서 계속 데이터를 받아와야한다.
2) StateFlow가 SharedFlow와 다른점 및 특징
- StateFlow는 SharedFlow의 구체화 버전이며, 발행된 마지막 데이터 값을 갖고 있다.
- StateFlow는 더 효율적이고 API가 더 간단하고, 상태 관리에 더 적절해서 ViewModel에서 일반적으로 더 자주 사용
- StateFlow는 1의 고정된 replayCache값을 가지며(LiveData와 유사), 구성 변경 시 기존 구독자뿐만 아니라 새 구독자에게 가장 최근의 데이터를 방출한다. (여러개를 동시에 발행한다면 맨 마지막 데이터를 보여줄 것이다.)
3) 2가지 MutableStateFlow 값을 변경하는 방법 (a) Value 프로퍼티 사용하기, (b) emit 사용하기
(a) Value 프로퍼티 사용하기
- 프로퍼티를 통해 값을 동기적으로 변경하고 접근 할 수 있다.
private val _coroutineData = MutableStateFlow(CoroutineUiState.success("")) val coroutineData: StateFlow<CoroutineUiState<String>> = _coroutineData.asStateFlow() private val _number = MutableStateFlow(0) // Value 프로퍼티 사용 fun loadDataWithValue() { viewModelScope.launch { try { _coroutineData.value = CoroutineUiState.loading() delay(1000) // 서버에서 데이터를 가져온다는 시간을 가상으로 val data = "Data With Value ${_number.value++}" // 실제 데이터 가져오기 _coroutineData.value = CoroutineUiState.success(data) } catch (e: Exception) { _coroutineData.value = CoroutineUiState.error(e.message.toString()) // 에러 상태 } } }
(b) emit 사용하기
- emit은 suspend 함수로 비동기 방식으로 사용
// emit() 함수 사용 fun loadDataWithEmit() { viewModelScope.launch { try { _coroutineData.emit(CoroutineUiState.loading()) delay(1000) // 서버에서 데이터를 가져온다는 시간을 가상으로 val data = "Data With Value ${_number.value++}" // 실제 데이터 가져오기 _coroutineData.emit(CoroutineUiState.success(data)) } catch (e: Exception) { _coroutineData.emit(CoroutineUiState.error(e.message.toString())) } } }
(c) 받는 부분
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { coroutineStateFlowViewModel.coroutineData.collect { result -> Log.d("CoroutineStateFlowViewModel", "result : $result") when (result.status) { CoroutineStatus.LOADING -> showLoadingBar() CoroutineStatus.SUCCESS -> setTextView(result.data) CoroutineStatus.ERROR -> showErrorMsg(result.message) } } }
StateFlow 같은 경우 emit하는 데이터가 이전 데이터와 같은 경우 collect 부분으로 다시 들어오지 않는다. 데이터가 같더라도 다시 collect 부분을 가야한다면 SharedFlow를 사용해야한다.
SharedFlow
SharedFlow는 다수의 수신자가 안전하게 사용할 수 있다. 또한 한번에 여러 값을 발행할 수 있다. 이것은 StateFlow와 다른 점이다.
- SharedFlow는 StateFlow의 일반화 버전이다.
- SharedFlow의 replayCache는 개발자가 직접 정의할 수 있으며 기본값은 0이다. 기본값을 사용하면 구성 변경 시 기존 구독자뿐만 아니라 새 구독자에게도 값이 방출되지 않는다
- SharedFlow는 시간이 지남에 따라 동일한 이벤트를 트리거해야 하는 경우(예: SnackBar 표시 이벤트, 탐색 이벤트, 네트워크 연결 불가능 이벤트 등)에 더 적합하도록 이후의 동일한 값을 재방출하는 방식으로 진행된다.
private val _sharedFlow = MutableSharedFlow<String>( replay = 1, // (새로운 구독자들에게 이전 이벤트 방출 여부 (0: 방출X, 1 = 방출O) extraBufferCapacity = 1, // 추가 버터 생성 여부 (1 = 생성) onBufferOverflow = BufferOverflow.DROP_OLDEST // 버퍼 초과시 처리 여부 ) val sharedFlow: SharedFlow<String> = _sharedFlow .shareIn( scope = viewModelScope, // viewModelScope로 생명주기 관리 started = SharingStarted.WhileSubscribed(1000), // 구독자가 없을 때 5초 후 정지 replay = 1 // 최근 값 1개 재생 ) fun loadSharedFlow() { viewModelScope.launch { try { _sharedFlow.emit("로딩중") delay(1000) // 서버에서 데이터를 가져온다는 시간을 가상으로 val data = "Data With Value ${_number.value++}" // 실제 데이터 가져오기 _sharedFlow.emit(data) } catch (e: Exception) { _sharedFlow.emit("error : $e") } } }
StateIn 과 SharedIn
1. StateIn
- StateIn은 Flow를 StateFlow로 바꿔준다.
- 콜드 스트림을 핫스트림으로 변경해준다.
val stateFlowWithStateIn: StateFlow<String> = makeStateFlowData() .stateIn( scope = viewModelScope, // CoroutineScope started = SharingStarted.WhileSubscribed(2000), // Flow 시작 시점 설정 initialValue = "Initial State" // 초기 값 ) fun makeStateFlowData(): Flow<String> = flow { delay(1000) emit("Loading") delay(1000) emit("Success : my data : ${getData("statein")}") }
- SharingStarted.WhileSubscribed(1000) 사용:
- 구독자가 없더라도 1초 동안 유지되며, 새 구독자가 생기면 중단 없이 데이터를 전달.
- 구독자가 없다면 1초 후 중단되지만, 다시 구독하면 재시작.
- SharingStarted.Eagerly : 즉시 값을 감지하기 시작한다. replay값이 0 이라면 모든 값을 유실한다. (쓰는 이유 X)
- SharingStarted.Lazily : 첫 번째 구독자가 나올 떄 감지하기 시작 첫 번째 구독자는 모든 값을 수신하는 것이 보장
- WhileSubscribed : 첫 번쨰 구독자가 나올 때 감지하기 시작하며, 마지막 구독자가 사라지면 플로우도 멈춤 마지막 구독자가 사라진 후 몇 초 뒤에 플로우를 멈출지 설정 가능 WhileSubscribed(5000)
- replay 매개변수:
- replay = 1 설정으로 가장 최근 값 1개를 새로운 구독자에게 즉시 전달
- 구독자가 중간에 연결되더라도, 처음부터 시작하지 않고 최근 값을 받을 수 있는 Hot Stream의 특성을 강화
여러가지 사용법을 찾아보고 고민해봤다.
아직까지 찾아본 것으로는 collect를 사용하지 않고 바로 value로 가져올수 있는 형태로 만들어줄수 있다.
다른 예제와 내가 직접 사용해보면서 좀 더 고민해봐야겠다.
2.SharedIn
// SharedFlow + StateIn val sharedFlowWithSharedIn: SharedFlow<String> = makeStateFlowData().shareIn( scope = viewModelScope, // viewModelScope로 생명주기 관리 started = SharingStarted.WhileSubscribed(2000), // Flow 시작 시점 설정 replay = 1 // 최근 값 1개 재생 )
'안드로이드 학습 > Android 기술면접 대비' 카테고리의 다른 글
안드로이드 아키텍처 (MVC) (0) 2024.12.09 안드로이드 앱 테스트 기본 - 2 (UI Test예제 포함) (0) 2024.12.07 안드로이드 Coroutine Flow - 2 (Flow 사용) (0) 2024.11.28 안드로이드 Coroutine Flow - 1 (Flow란?) (0) 2024.11.27 안드로이드 코루틴 (Coroutine Builder) 코드 (0) 2024.11.18