Flow?
Android 에서 Coroutine의 Flow는 데이터 스트림이며, 코루틴 상에서 리액티브 프로그래밍을 지원하기 위한 구성요소이다.
Flow는 코루틴을 기반으로 여러 값을 제공할 수 있다. 비동기식으로 계산할 수 있는 데이터 스트림의 개념이다. 따라서 반환 값은 동일한 유형이어야 한다.
ex) Flow<Int>는 정수 값을 내보내는 흐름이다.
Flow는 suspend함수를 사용하여 값을 비동기적으로 생성하고 사용한다. 예를 들어 Flow는 Default 스레드를 차단하지 않고 다음 값을 생성할 네트워크 요청을 안전하게 만들 수 있다.
코루틴에서 비동기적으로 호출하기 위한 suspend 함수와 동일하지만 flow에서는 suspend 를 붙일 필요가 없다.
리액티브 프로그래밍이란?
== 반응형 프로그래밍
리액티브 프로그래밍이란 데이터가 변경 될 때 이벤트를 발생 시켜 데이터를 계속 전달하도록 하는 프로그래밍 방식을 말한다.
이는 기존의 명령형 프로그래밍과 대응되는 개념이다.
명령형 프로그래밍 vs 리액티브 프로그래밍
명령형 프로그래밍에서 데이터의 소비자는 데이터를 요청 후 받은 결과 값을 일회성으로 수신한다. 하지만, 이런 방식은 데이터가 필요할 때마다 결과 값을 매번 요청해야 한다는 점에서 비효율적이다.
그러나 리액티브 프로그래밍은 데이터를 발행하는 발행자가 있고, 데이터의 소비자가 발행자에 구독을 요청한다. 그러면 데이터의 발행자는 새로운 데이터가 들어오면 데이터를 소비자에게 지속적으로 발행한다. 따라서 데이터 필요할 때마다 결과값을 매번 요청하지 않아도 되므로 효율적인 서비스를 만들 수 있다.
⇒ 사용자의 동작에 대해 일일히 컴퓨터가 어떤 동작을 해야 하는지 알려주지 않아도 된다
데이터 스트림
즉 리액티브 프로그래밍에는 하나의 데이터를 발행하는 발행자가 있고 해당 발행자는 데이터의 소비자에게 지속적으로 데이터를 전달하는 역할을 한다.
이를 데이터 스트림이라 부른다.
Coroutine에서 리액티브 프로그래밍
코루틴 Flow는 코루틴 상에서 리액티브 프로그래밍을 지원하기 위해 만들어진 구현체이다. 코루틴에서 데이터 스트림을 구현하기 위해서는 Flow를 사용해야 한다.
데이터 스트림의 구성요소를 살펴보면 크게 3가지로 구성되어있다.
- Producer(생산자)
- Intermediary(중간 연산자)
- Consumer(소비자)
Producer(생산자)
생산자는 데이터를 발행하는 역할을 한다.
Flow에서 생산자는 flow { } 블럭 내부 emit()을 통해서 데이터를 생산한다.
안드로이드 상에서 생산자가 가져오는 데이터의 대표적인 DataSource는 RemoteSource, LocalSource 2가지가 있다.
미세먼지 앱을 만든다고 가정하면, 아래와 같은 과정을 거치게된다.
- 먼저 flow { } 블록을 선언한다.
- 미세먼지 데이터를 서버로부터 받아온다. (Remote DataSource)
- Producer가 데이터를 생성한다. (emit)
- 2~3과정을 60초마다 반복하여 데이터를 계속해서 생성한다. (delay 통해)
class DustRemoteDataSource(
private val dustApi: DustApi
) {
fun getDustInfoFlow() : Flow<List<DustInfo>> = flow { // 1.Flow 블록 선언
while(true) {
val dustInfos = dustApi.fetchLastedDustInfo() //2. 데이터 받아오기
emit(dustInfos) //3. Producer가 데이터 발행
delay(INTERVAL_REPRESH) //4. 60초마다 반복
}
}
companion object {
private const val INTERVAL_REPRESH: Long = 60000
}
}
Intermediary(중간 연산자)
중간 연산자는 생산자가 데이터를 생성했으면 생성된 데이터를 수정한다.
예를 들어 A라는 객체로 이루어진 데이터를 발행했는데 B라는 객체 데이터가 필요한 경우 Flow에서 지원하는 중간 연산자를 이용해 A객체를 B객체로 바꿀 수 있다.
대표적인 중간 연산자는 map(데이터 변형), filter(데이터 필터링), onEach(모든 데이터마다 연산 수행) 등의 중간 연산자가 있다.
미세먼지 앱에서 View에 전달할 때 모든 처리가 완료된 데이터가 전달되는 것이 좋다.
우리는 모든 DustInfo가 필요한 것이 아닌, 지역 미세먼지 데이터만 필요하다고 가정해보자. 따라서 우리는 우리 지역 미세먼지 데이터를 제외한 모든 데이터를 filter하기 위해 기존 데이터 중 필요한 데이터로 변형하여 방출하는 map을 사용하고, 들어온 데이터를 filtering하여 특정 지역의 데이터를 방출한다.
class DustRepository(
private val dustRemoteDataSource: DustRemoteDataSource,
){
fun getDustInfoOfViewItem(locale : Locale) = //locale은 특정 지역을 담은 변수이다.
dustRemoteDataSource.getDustInfoFlow().map { it.filter { this.locale == locale } }
Consumer(소비자)
중간 연산자는 생산자가 생성한 데이터를 변환하여 소비자로 데이터를 전달한다.
Flow에서는 collect를 이용해 전달된 데이터를 소비할 수 있다.
Flow는 Cold Stream으로 flow 내부 코드는 collect()가 호출되어야 실행된다.
안드로이드 상에서 데이터 소비자는 보통 UI 구성요소이다.
UI는 데이터를 소비하여 데이터에 맞게 UI를 그려낸다.
받은 미세먼지 데이터를 이용해 ViewModel에서 필요한 처리를 하여 View에서 사용하면 된다.
class DustViewModel(
private val dustRepository: DustRepository
) : ViewModel() {
fun collectDustInfoOf(locale: Locale) =
viewModelScople.launch {
dustRepository.getDustInfoOf(locale).collect { dustInfos ->
items.value = dustInfos //예시임
}
}
}
LiveData vs Flow
Flow 와 LiveData는 API를 통해 데이터를 받아올 때 자주 사용되는 요소들이다.
간단히 정리하면
Flow : 여러 값을 내보내는 데이터 스트림이며 코루틴 기반으로 빌드된다.
LiveData : 안드로이드 생명주기를 인식하는 데이터 홀더 클래스이다. 따라서 액티비티, 프래그먼트에서 사용하기 좋다.
LiveData 개요 | Android 개발자 | Android Developers
위 공식문서를 살피면 이런 부분이 나온다.
activity와 프래그먼트는 상태 보유가 아닌 데이터를 표시하는 역할을 하므로 LiveData 인스턴스를 보유해서는 안 됩니다. activity와 프래그먼트가 데이터를 보유하지 않도록 하면 단위 테스트를 작성하기도 쉬워집니다.
데이터 영역 클래스에서 LiveData 객체를 작업하고 싶을 수 있지만 LiveData는 비동기 데이터 스트림을 처리하도록 설계되지 않았습니다. LiveData 변환과 [MediatorLiveData](<https://developer.android.com/reference/androidx/lifecycle/MediatorLiveData?hl=ko>)를 사용하여 LiveData 객체 작업을 할 수는 있지만 이 접근 방식에는 단점이 있습니다. 즉, 데이터 스트림을 결합하는 기능이 매우 제한적이고 변환을 통해 만들어진 객체를 포함하여 모든 LiveData 객체가 기본 스레드에서 관찰됩니다.
. . .
앱의 다른 레이어에서 데이터 스트림을 사용해야 한다면 Kotlin Flow를 사용한 다음 asLiveData()를 이용해 ViewModel의 LiveData로 변환하는 것이 좋습니다.
즉 데이터 스트림을 써야한다면 Flow를 사용한 다음 asLiveData()로 LiveData의 변환을 권장하고있다. Flow를 쓰라는 뜻이다.
또한 LiveData는 모든 LiveData 객체의 관찰이 메인 쓰레드에서 이루어진다는 것이다.
ViewModel을 통해 뷰를 업데이트 하는 경우에는 문제가 안될 수 있지만 Data Layer에 사용하면 문제가 된다.
데이터를 받아오는 곳이 서버 또는 DB라면 무거운 작업이 되기 때문에 메인이 아닌 별도 워커쓰레드에서 처리해야 ANR(Application Not Responding) 등을 피할 수 있다.
또한 아키텍처의 관점에서 LiveData는 문제점을 가진다.
요약을 하자면 클린 아키텍처 관점에서 LiveData 의 문제점은 아래와 같다.
- LiveData 는 UI 에 밀접하게 연관되어 있기 때문에 Data Layer 에서 비동기 방식으로 데이터를 처리하기에 자연스러운 방법이 없다.
- LiveData 는 안드로이드 플랫폼에 속해 있기 때문에 순수 Java / Kotlin 을 사용해야 하는 Domain Layer 에서 사용하기에 적합하지 않다.
LiveData와 Flow는 서로를 보완하는 역할을 한다. LiveData는 구성 변경 시에 안정성을 제공하고 최신 데이터를 뷰로 전달하는 역할을 한다. Flow는 UseCase, Repository, DataSources Layer와 긴밀하게 작동해 데이터를 수집 및 처리해서 서로 다른 코루틴 범위에서 작업을 실행한다. 그래서 ViewModel, View 사이의 상호작용은 LiveData, 더 깊은 레이어와 쓰레딩과 같은 더 복잡한 처리는 Flow가 처리한다.
Flow vs StateFlow
위 LiveData의 단점을 보완하기 위해 Flow를 사용할 수 있으나 대체가 쉬운일이 아니다.
- Flow는 스스로 안드로이드 생명주기를 알지 못한다. 따라서 생명주기에 따른 중지, 재개가 어렵다.
- Flow는 상태가 없어 값이 할당되었는지, 현재 값은 무엇인지 알기가 어렵다
- Flow는 콜드 스트림 방식이다. 연속해서 계속 들어오는 데이터를 처리할 수 없고 collect 되었을 때만 생성되고 값을 반환한다.
→ 만약 하나의 Flow Builder에 대해 다수의 collector가 있다면 collector 하나마다 하나씩 데이터를 호출하기 때문에 비용이 비싼 DB접근, 서버 통신 등을 수행한다면 여러 번 리소스 요청을 하게 될 수 있다.
이를 위해 코틀린 1.41에 Stable API로 등장한 것이 StateFlow, SharedFlow다.
StateFlow란
현재 상태와 새로운 상태 업데이트를 수집기에 내보내는 관찰 가능한 상태 홀더 흐름이다.
value 속성을 통해 현재 상태값을 읽을 수 있다.
상태를 업데이트하고 flow에 보내려면 MutableStateFlow 클래스의 value 속성에 새 값을 할당한다. 이는 관찰 가능한 변경 가능 상태를 유지해야하는 클래스에 아주 적합하다.
== 데이터 홀더(저장소) 역할을 하면서 Flow의 데이터 스트림 역할까지 한다.
UI단에서 State Flow를 구독해 UIState(ui를 위한 데이터)를 업데이트 하면 화면이 재구성될 때마다 다시 서버에 데이터를 요청할 필요가 없어진다. UI는 단순히 StateFlow만 구독하고 있으면 되는 것이다.
코틀린 Flow의 예시를 따라 View가 UI 상태 업데이트를 listen하고 구성 변경에도 기본적으로 화면 상태가 지속되도록 LatestNewsViewModel에서 StateFlow를 노출할 수 있다
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()
}
'Android > 공부' 카테고리의 다른 글
[Android/공부] JNI 활용 안드로이드 디버깅 / USB 탐지 (JAVA) (0) | 2024.07.11 |
---|---|
[Android/공부] 안드로이드 소스 코드 난독화 R8 / Proguard (1) | 2024.07.10 |
[Android/코루틴] 2.코루틴의 기본요소들을 알아보자 (0) | 2023.09.21 |
[Android/코루틴] 1. 코루틴은 무엇이고 왜 쓰는가? (0) | 2023.09.21 |
[Android] 첫 안드로이드 앱 릴리즈 과정에서 겪은 트러블슈팅들 (0) | 2023.09.11 |