Skip to content

Flow 적용기

박찬호 edited this page Dec 14, 2022 · 1 revision

💡 Flow를 이해하기 위해서는 코루틴 개념을 이해하고 있어야 합니다.

Flow

  • 비동기로 동작하기 위한 Coroutine의 데이터 스트림

  • 기존 데이터 단일 값으로 일회성 수신을 이루었던 코루틴의 suspend방식과는 다르게 여러 값을 순차적으로 내보내는 유형

  • Iterator와 유사하고, suspend를 사용하여 비동기적으로 사용된다.

  • 일반적으로 Flow의 경우 Cold 스트림으로 분류된다.

  • Cold Stream VS Hot Stream

    🧊 Cold Stream 🔥 Hot Stream
    하나의 Consumer(소비자)에게 보낸다. 하나 이상의 Consumer(소비자)에게 보낸다.
    생성된 이후 누군가 소비하기 시작하면 그때 데이터를 발행(emit)한다. 구독자 유무에 상관없이 데이터 발행(emit) 시작 이후 모든 Consumer에게 동일한 데이터를 계속해서 발행한다. (구독자가 없어도 발행)

데이터 스트림(Streams of Data) 이해하기

데이터 스트림 (출처 : Android Developers)

데이터 스트림의 3가지 요소

1️⃣ Producer 생산자

  • 스트림 데이터를 생산하는 역할
  • 코루틴으로 동작할 수 있기 때문에 비동기적으로 생산될 수 있음

[Android Arch.] 일반적으로 DataSource에서 생산하거나 호출

class NewsRemoteDataSource(
    private val newsApi: NewsApi,
    private val refreshIntervalMs: Long = 5000
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        while(true) {
            val latestNews = newsApi.fetchLatestNews()
            emit(latestNews) // 네트워크 결과를 Flow로 발행
            delay(refreshIntervalMs)
        }
    }
}

// Retrofit 네트워크 비동기 통신으로 인터페이스 구현
interface NewsApi {
    suspend fun fetchLatestNews(): List<ArticleHeadline>
}
  • 네트워크를 통해 비동기적으로 호출한 결과를 emit()하여 발행할 수 있다.
  • Flow는 Sequential(순차적)으로 동작하기 때문에 네트워크 요청이 끝날 때까지는 정지 상태로 남는다. 그 후에 결과를 스트림으로 내보낸다.

2️⃣ Intermediaries 중재자

  • 생산자가 만든 데이터 스트림 각각의 값이나 스트림 자체를 수정

[Android Arch.] 일반적으로 Repository에서 작업

class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val userData: UserData
) {
    val favoriteLatestNews: Flow<List<ArticleHeadline>> =
        newsRemoteDataSource.latestNews
            // map을 활용하여 좋아하는 토픽만 필터링
            .map { news -> news.filter { userData.isFavoriteTopic(it) } }
            // onEach를 활용하여 스트림 값을 캐시에 저장
            .onEach { news -> saveInCache(news) }
}
  • Flow를 통해 생산된 스트림 값이나 스트림 자체를 정제하는 작업을 수행한다.
  • 체인 형태의 중간 연산자를 차례대로 적용하여 소비자에게 선택적으로 내보낼 수 있다.

3️⃣ Consumer 소비자

  • 발행되는 스트림의 값을 사용

[Android Arch.] 일반적으로 ViewModel에서 소비

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    init {
        viewModelScope.launch {
            // Flow를 collect()하여 내보낸 값을 소비
            newsRepository.favoriteLatestNews.collect { favoriteNews ->
                // 요청된 데이터를 통해서 View 업데이트
            }
        }
    }
}
  • 일반적으로 viewModelScope에서 소비하게 되면 생산자에게 내보낼 것을 요청하게 되고 생산자는 while(true) 루프 상태로 활성 상태가 유지된다.
    • 즉, collect()를 통해 소비하기 시작하면 데이터 스트림이 종료되기 전까지 해당 Flow를 계속해서 생산하는 상태가 된다는 것이다.
  • 다음과 같은 상황에서 데이터 스트림이 종료된다고 한다.
    • ViewModel이 없어져 코루틴(viewModelScope)가 취소될 때
    • 생산자가 데이터 스트림 emit()을 완료하여 값이나 오류를 내보냈을 때

💡 collect()

  • 생산된 Flow 데이터 스트림을 소비하는 함수이다.
  • suspend함수에 해당하므로 코루틴 내에서 사용해야 한다.

✅ 예외 처리

// Repository나 ViewModel에서..
val favoriteLatestNews: Flow<List<ArticleHeadline>> =
        newsRemoteDataSource.latestNews
            .map { news -> news.filter { userData.isFavoriteTopic(it) } }
						.catch { exception -> notifyError(exception) }
  • catch()연산자를 통해 데이터 스트림의 값을 제대로 수신하지 못하게 된 경우, exception에 따라 예외처리를 작업할 수도 있다.

✅ 디스패쳐 지정하기 (다른 코루틴 Context로 실행하기)

// Repository에서..
val favoriteLatestNews: Flow<List<ArticleHeadline>> =
        newsRemoteDataSource.latestNews
            .map { news -> news.filter { userData.isFavoriteTopic(it) } }
            // ↑ flowOn 이전에는 지정한 Dispatcher로 동작
            .flowOn(defaultDispatcher)
            // ↓ flowOn 이후에는 상관없이 Default로 동작
            .catch { exception -> emit(lastCachedNews()) }
  • 일반적으로 Repository에서 ioDispatcher로 데이터 스트림 입출력 작업을 수행하기 위해 작성된다.

💡 하지만 대부분 우리가 사용하는 라이브러리에서는 내부적으로 CoroutineContext를 전환해주고 있다! 꼭 라이브러리를 사용할 때 알고 사용하도록 하자!

ex) Retrofit, Room, Firebase..


📂Reference

Kotlin flows on Android | Android Developers

[Coroutine Flow] 1. Flow란 무엇인가?

Kotlin coroutine flow

Clone this wiki locally