우선 코루틴에 대해서 기본은 알고 있다고 생각하고 넘어가겠다. 또한, 코루틴에 대한 기본 및 심화는 추가로 올리는걸로..
Android Kotlin Flow Definition
코루틴에서의 Flow는 단일 값만 반환하는 suspend function과 달리 여러 값을 순차적으로 내보낼 수 있는 유형이다.
Ex) Flow를 사용하여 데이터베이스에서 실시간 업데이트를 수신할 수 있다.
Flow는 코루틴을 기반으로 빌드되며 어려 값을 제공할 수 있다. 또한! 내보낸 값은 동일한 유형이어야 한다.
Ex) Flow<Int>는 정수 값을 내보내는 Flow이다.
Flow는 값 시퀀스를 생성하는 Iterator와 매우 비슷하지만 suspend function을 사용하여 값을 비동기적으로 생성하고 사용한다.
Ex) Flow는 기본 스레드를 차단하지 않고 다음 값을 생성할 네트워크 요청을 안전하게 만들 수 있다.
즉, Coroutine의 Flow는 Data Stream이며, Coroutine에서 리엑티브 프로그래밍을 할 수 있도록 지원하기 위한 구성요소이다.
간단하게 리엑티브 프로그래밍에 대해 알아보자.
What is Reactive Programming?
리엑티브 프로그래밍이란 데이터가 변경될 때 이벤트를 발생시켜 데이터를 계속해서 전달하도록 하는 프로그래밍 방식을 뜻한다.
기존 명령형 프로그래밍에서는 Data Consumer는 데이터를 요청한 후 받은 결과값을 일회성으로 수신한다. 하지만, 이러한 방식은 데이터가 필요할 때마다 결과값을 매번 요청해야한다는 점에서 매우 비효율적이다.
리엑티브 프로그래밍에서는 데이터를 발행하는 발행자가 있고, 데이터의 컨슈머는 데이터의 발행자에게 구독 요청을 한다.
그러면! 데이터의 발행자는 새로운 데이터가 들어오면 데이터의 소비자에게 지속적으로 발행한다.
즉, Reactive Programming에는 하나의 데이터를 발행하는 발행자가 있고 해당 발행자는 데이터의 소비자에게 지속적으로 데이터를 전달하는 역할을 한다. 이를!! Data Stream이라 한다.
데이터 스트림에는 다음과 같은 세 가지 항목이 있다.
1. 생산자(Producer)는 스트림에 추가되는 데이터를 생산한다. 코루틴 덕분에 흐름에서 비동기적으로 데이터가 생산될 수 있다.
2. (선택사항) 중개자(Intermediary)는 스트림에 내보내는 각각의 값이나 스트림 자체를 수정할 수 있다.
3. 소비자(Consumer)는 스트림의 값을 사용한다.

예를들어, 날씨 앱을 만든다고 가정한다면 아래와 같은 과정을 거친다.
1. Flow 선언
2. 날씨 데이터를 서버로부터 가져온다.
3. Producer가 데이터르를 생성한다.
4. 2,3번 과정을 30초마다 반복하여 데이터를 계속 생성
class WeatherRemoteDataSource(private val weatherApi: WeatherAPI){
fun getWeatherFlow(): Flow<List<Wheather>> = flow{
while(true){
val weeklyWeather = weatherApi.fetchWeekWeather()
emit(weeklyWeather)
delay(INTERVAL_REFRESH)
}
}
companion object{
private const val INTERVAL_REFRESH: Long = 30000
}
}
Intermediary(중간 연산자)
생산자가 데이터를 생성했으면 중간 연산자는 생성된 데이터를 수정한다.
Ex) 예를들어 생성자는 A라는 객체로 이루어진 데이터를 발행했는데 B라는 객체가 필요한 경우 Flow에서 지원하는 중간 연산자를 이용해 A객체를 B객체로 바꿀 수 있다.
중간연산자로는 map, filter, onEach 등이 있다.
다시 앞선 날씨 앱을 보면, View에 전달할 때는 processing된 데이터가 전달되어야 한다.
사실 Wahter Data Class에 11개의 변수가 있다고 가정하자. 그 중 전부가 필요하지 않고 특정 위치(locale)의 날씨 데이터만 필요하다.
따라서 중간 연산자를 통해 원하는 데이터만 가져온다.
class WeatherRepository(private val weatherRemoteDataSource: WeatherRemoteDataSource){
fun getWeatherItem(locale: Locale) =
weatherRemoteDataSource.getWeatherFlow().map{ it.filter{ this.locale == locale}}
}
Consumer(소비자)
Intermediate가 데이터를 수정한 후 소비자에게 데이터를 전달한다. 이후 Flow에서는 collect를 이용해 전달된 데이터를 소비할 수 있다.
안드로이드 다큐먼트에 따르면 Data Consumer는 UI Layer에 있는 UI Component가 데이터를 소비하여 이에 맞는 UI를 그린다.
이제 받은 날씨 데이터를 이용해 ViewModel에서 필요한 처리를 한 후 UI State에 띄어준다.(UI Layer 작용 방식)
class WeatherViewModel(private val weatherRepository: WeatherRepository): ViewModel(){
fun collectWeather(locale: Locale) =
viewModelScope.launch{
weatherRepository.getWeather(locale).collect{ weatherInfo ->
//...///
}
}
}
}
Flow의 한계
Flow는 앞서 코루틴이 리엑티브 프로그래밍을 지원하기 위해 만든 Data Stream이다.
하지만, Flow는 Data Stream을 발생시키기만 할 뿐 데이터가 저장되지 않는다...
따라서, Flow만을 사용해서 Android UI State에 띄우기 위한 방법으로는 두 가지가 있다.
1. 화면이 refresh 될때마다 다시 Server, DB로부터 데이터 가져오기
-> 상기 방식은 비효율적이다. 안드로이드의 생명주기를 따라 화면이 회전되거나 다른 행위를 하고 다시 돌아왔을때 onDestroy가 호출된 후 onCreate가 호출되는데, 이 과정마다 새로운 데이터를 가져와야 하기 때문이다..서버 리소스 비용 급상승....
2. Flow로부터 collect한 데이터를 ViewModel에 저장해놓고 사용하기
-> ViewModel은 생명주기를 고려해 onDestroy가 호출되더라도 살아있기에 데이터를 저장시킬 수 있다!
그러나, 2번 방법인 viewModel 저장에서 데이터를 저장하고 있으려면 별도의 Holder를 생성해야 한다. 또한 Holder는 Reactive하지 않기 때문에 UI에서 해당 Holder를 사용하기 위해 별도의 fetching function을 만들어야 한다..
또 다른 방법으로는 viewModel에서 Holder와 flow를 같이 사용하는 방법이다. flow를 사용하고 Holder는 flow에서 마지막으로 발행한 데이터를 저장하고 있으면 된다. 따라서 UI에서는 flow에서 값을 발행하기 전에는 Holder의 데이터를 사용하면 된다.
이 방식으로 만들면 Holder가 마지막 데이터를 갖고 있어 다시 서버로 데이터를 요청할 필요가 없어진다.
하지만! 위 두 방식은 Boiler Plate Code를 만든다는 것이다..... 안드로이드에서 UIState가 단일이 아닌데 모두를 갖기 위해 유사 코드를 매번 작성해 가독성을 떨어뜨릴 수 있기 때문..
상기 문제점들을 해결해 줄 수 있는 것이 State Flow이다.
StateFlow Definition
StateFlow는 현재 상태와 새로운 상태 업데이트를 Collector에 내보내는 Observable한 StateHolder Flow이다.
즉, StateFlow는 Data Holder(Repository) 역할을 하면서 Flow의 Data Stream 역할까지 한다는것.
UI Layer에서 StateFlow를 갖고 UIState에 업데이트 하면 화면이 재구성될 때마다 다시 서버로 데이터를 요청할 필요가 없어진다.
결국, UI는 단순히 StateFlow를 구독만 하고 있으면 된다는 것..
Flow to StateFlow
보통 ReativeProgramming을 할 때 다양한 Flow를 하나의 Flow로 합친다.
예를 들어 피트니스 회원 주간 성과표를 만들 경우, 1주일간의 운동 데이터, 1주일간의 식단 데이터, 1주일 간의 섭취 칼로리 등 성과표를 만드는 데이터를 갖기위한 정보를 각 테이블에서 가져와 하나의 객체로 만들어줘야 한다..
이렇게 합쳐 만들어진 Flow는 UI가 단순히 구독하기 위한 StateFlow로 변환해주어야 한다.
추가로 StateFlow가 Flow를 계속 구독하고 있으면 Memory Leak가 생기므로 Coroutine Scope를 통해 StateFlow의 수명을 관리하어야 한다.
우선 Flow를 StateFlow로 변환하려면 stateIn 중간연산자를 사용한다.
stateIn Definition
fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T
): StateFlow<T>
ColdFlow를 지정된 코루틴 범위에서 시작되는 hotStateFlow로 변환하여 업스트림 흐름의 단일 실행 인스턴스에서 가장 최근에 내보낸 값을 여러 다운스트림 구독자와 공유합니다.
다음을 사용하기 위해
scope: StateFlow가 Flow로부터 데이터를 구독받을 CoroutineScope를 명시한다.
stated: Flow로부터 언제 구독을 할지 명시한다.
initialValue: StateFlow에 저장될 초기값을 설정한다.
사용방법은 다음과 같다.
val exampleFlow: FLow<Int> = flow{
for(i in 0..10){
emit(i*i)
delay(400)
}
}
상기 flow를 stateIn 중간 연산자를 사용해 StateFlow로 변환해보자
val stateFlow = stringFlow.stateIn(
initialValue = 0,
started = SharingStarted.WhileSubscribed(1000),
scope = viewModelScope
)
이로써, 초기 저장 값은 이고, 구독 후 1초 뒤 처음 발행, 또한, viewModel의 생명주기만큼 구독받는 행동을 하는 StateFlow가 만들어진다.
'Computer engineering > Android Programming' 카테고리의 다른 글
REST API_HttpURLConnection_Coroutine (0) | 2022.07.13 |
---|---|
AAC_ViewModel2 (0) | 2022.07.12 |
AAC_ViewModel_1 (0) | 2022.07.12 |
MVVM with Clean Architecture (0) | 2022.07.11 |
4. Architecture Component UI Layer Data Binding_3 (1) | 2022.07.10 |