본문 바로가기

안드로이드

Flow (안드로이드)

Flow란?

-kotlin에서 제공해주는 비동기 통신을 도와주는 기능이다. 안드로이드에서 자주 사용되고 보통 반응형 UI를 만드는데 자주 사용된다.
보통 서버에서 데이터를 받아오거나 로컬DB에서 데이터를 받아올때 많이 사용된다.
Flow는 코루틴 위에서 동작하기 때문에 본인이 코루틴에 관해서 잘 모른다면 먼저 코루틴에 관해서 선행학습을 해야할 필요가 있다.

Flow의 구성요소

Flow에는 세가지 구성요소가 있다.
Producer : 번역하면 생산자인데, 보통 repository를 일컫는다. repository에는 서버나 로컬에서 받아온 데이터를 갖는 저장소를 의미한다. Flow가 코루틴 위에서 동작하기 덕분에 데이터를 비동기적으로 받아올 수 있다.
Intermediary : 번역하면 중재자이다. 이 녀석은 필수가 아닌 선택요소이다. 반드시 필요한 녀석은 아니란 말이다. 다른 구성요소로 가는 데이터를 변형시킬 수 있다.

(사실 말이 선택사안이지 쓰는게 개발하는데 훨씬 좋다. map, onEach, filter, catch 등이 존재한다. 보통 예제들을 살펴보면 레트로핏을 통해서 받아온 데이터의 타입이 <List<DataClass>> 이런 식인 경우가 많은데 map 메소드를 사용해서 각 list에 담긴 각 인덱스별 결과값에 대해서 처리하는게 가능하다. 그리고 통신오류가 발생하면 catch중재자를 사용해서 예외 처리하는 것도 가능하겠지.)

Consumer : 소비자. 여기선 UI를 의미한다. 안드로이드에선 Activity나 Fragment가 되겠다. Stream에서 받아온 데이터를 최종적으로 소비하기 때문에 소비자(consumer)라고 부른다.
 

Flow를 생성, 사용하는 방법

Flow를 생성하기 위해서는 flow builder를 사용해야 한다. flow builder를 사용하면 외부로부터 데이터를 요청하고 받아오는 일이 가능하다.
레트로핏을 써서 Remote에서 받아오거나 룸, SQLite등을 사용해서 Local에서 받아오는게 가능하다.
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) // Emits the result of the request to the flow
            delay(refreshIntervalMs) // Suspends the coroutine for some time
        }
    }
}

// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
    suspend fun fetchLatestNews(): List<ArticleHeadline>
}
flow builder는 코루틴 영역에서 실행된다. 비동기 통신을 했을때 장점을 사용할 수 있지만 몇가지 제한사항이 있다.
  • Flow는 순차적이다. producer는 코루틴 영역에 있기 때문에, suspend 메소드가 리턴값을 받아오기 전까지 producer가 suspend 메소드를 중단시킨다.
    (서버나 로컬에서 리턴값을 받기 전까지는 suspend함수가 중지되버림)
  • flow builder 를 사용하면 다른 CoroutineContext에서 값을 받아올 수 없다. (다른 코루틴 영역에서 flow를 활용해서 외부 데이터를 갖고올 수 없다는 말이다.)
    (이 경우 callBackFlow를 사용하면 된다고 한다. 만약 본인이 FireBase 의 FireStore를 공부해봤다면 구글 공식문서에서 해당 내용을 살펴보는 것을 추천한다. 써본 사람 입장에선 꽤 쉽다.)

Stream을 변형시키는 방법(Modify the stream)

앞서 언급했던 중재자를 사용한다. 중재자를 사용하면 값의 흐름(stream)에서 개발자가 원하는 입맛대로 데이터를 변형하는게 가능하다.
 
커뮤니티 앱을 제작중이라고 가정해보자. 사용자들이 작성한 게시글을 전부다 Flow를 통해서 불러오는데, 이때 map 메소드를 사용해서 개발자가 원하는 데이터로 필터링 하는게 가능하다. 이게 곧 "Modify the stream"에 해당하는 내용이다. map을 통해서 좋아요 개수가 100개 이상인 인기글만 필터링하는게 목적이라면 이럴때 아주 유용한 것이다.
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val userData: UserData
) {
    /**
     * Returns the favorite latest news applying transformations on the flow.
     * These operations are lazy and don't trigger the flow. They just transform
     * the current value emitted by the flow at that point in time.
     */
    val favoriteLatestNews: Flow<List<ArticleHeadline>> =
        newsRemoteDataSource.latestNews
            // Intermediate operation to filter the list of favorite topics
            .map { news -> news.filter { userData.isFavoriteTopic(it) } }
            // Intermediate operation to save the latest news in the cache
            .onEach { news -> saveInCache(news) }
}
위의 코드는 공식문서에서 뉴스글에서 사용자가 좋아요 처리한 값만 필터링 하는 예시인데, 중재자 역할을 해줄 수 있는 메소드는 매우 유용하니까 익숙해지자.
map, onEach, filter, catch 메소드 등

 

flow를 통해서 값을 받는 방법(Collecting from flow)

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

    init {
        viewModelScope.launch {
            // Trigger the flow and consume its elements using collect
            newsRepository.favoriteLatestNews.collect { favoriteNews ->
                // Update View with the latest favorite news
            }
        }
    }
}
위의 코드는 viewModel에서 repository에서 받아온 데이터를 collect를 사용해 받는 코드이다.
flow는 주기적으로 외부에서 값을 받아오는 기능을 계속 동작시킨다. producer가 동작하고 있는한 while(true)를 통해서 해당 기능을 계속 수행하기 때문이다. 뷰모델이 사라지거나 코루틴의 ViewModelScope가 취소되면 이 기능을 멈춘다.

 

예상하지 못한 예외 잡기(Catching unexpected exceptions)

외부에서 데이터를 갖고 올때 다양한 원인으로 인해서 예상치 못하게 값을 제대로 못갖고 오는 경우가 있다. 이런 경우를 대비해서 Flow 에서는 중재자에서 catch 연산을 지원해준다.
외부 라이브러리를 사용할때 매우 유용한데 예를 들면 레트로핏 처럼 외부에서 값을 갖고 오는 경우다.
class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                // Intermediate catch operator. If an exception is thrown,
                // catch and update the UI
                .catch { exception -> notifyError(exception) }
                .collect { favoriteNews ->
                    // Update View with the latest favorite news
                }
        }
    }
}
 
 
출처

 

아래 링크는 주인장이 직접 만들어본 예제다.
조금 부족한 부분이 있거나 아쉬운 부분있다면 댓글로 남겨주세요.

https://github.com/YunSeokVV/RetrofitPractice/tree/Flow