Flow에서 생산자는 데이터를 Flow에 입력하고 소비자는 Flow에서 데이터를 수집하는 역할을 가지고 있다.
Android에선 데이터 소스나 레포지토리가 전형적인 생산자이고, 최종적으로 화면에 데이터를 표시하는 UI가 소비자 역할을 한다.
Creating Flows
그렇다면 우선 Flow를 생성하는 방법을 알아보자. 대부분의 경우 Flow를 직접 만들 필요는 없으며, 데이터 소스 라이브러리는 보통 Flow와 통합되어 있다.
DataSource, Retrofit, Room, WorkManager 등이 존재한다. 이들은 댐 역할을 수행하며 Flow를 사용하여 데이터를 제공하고, 개발자는 구현 방법을 몰라도 파이프에 연결만 하면 데이터를 사용할 수 있다.
Room을 예시로 들어보자.
위 코드에서 List<Codelab>타입의 Flow를 노출하여 데이터베이스 변경 사항을 알리고 있다. Room 라이브러리가 생산자 역할을 맡아 업데이트가 있을 때마다 쿼리 내용을 전달한다.
만약 Flow를 직접 만들어야 한다면, 우리는 여러가지 방법을 사용할 수 있다.
그 중 하나가 Flow 빌더이다. 수시로 앱에서 온 메시지를 확인하려는 예시를 가정으로 들어보자.
우리는 메시지 리스트 타입의 flow로 사용자 메시지를 노출할 수 있다.
이 때 flow를 만드려면 flow 빌더를 사용해야 한다. flow 빌더는 suspend 함수를 호출 할 수 있다. 이는 flow가 코루틴 컨텍스트에서 실행되기 때문에 가능하다.
내부에서 while(true) 루프로 반복하게 한 후, 먼저 API에서 메시지를 가져온다.
그리고 emit 함수를 통해 가져온 메시지를 노출한다. 이렇게 하면 컬렉터가 아이템을 받을 때까지 코루틴을 중단시킨다.
마지막으로 일정 시간 코루틴을 중단한다. 위 flow는 동일한 코루틴에서 연산을 순차적으로 진행하며 관찰자가 사라지고 아이템 수집이 중단되면 멈추게 된다.
flow 빌더에 전달된 suspend 블록은 생산자 블록이라고도 한다.
Android에서 생산자와 소비자 간의 계층은 요구 사항에 맞게 데이터 스트림을 수정할 수 있으며, flow를 변환하려면 중간 연산자를 사용하면 된다.
latestMessages 스트림이 flow의 시작점이라고 가정한다면 map 연산자를 사용해서 데이터를 다른 타입으로 변환할 수 있다.
이를 통해 더 좋은 추상화를 제공할 수 있게 된다.
각 연산자는 기능에 따라 데이터를 전송하는 새로운 flow를 생성한다.
filter를 사용하여 중요한 알림이 포함된 메시지의 flow를 가져올 수도 있다.
그렇다면 스트림에서 발생하는 에러는 어떻게 처리할 수 있을까?
catch 연산자는 업스트림 flow에서 발생할 만한 예외를 찾아낸다.
업스트림 flow란 생산자 블록에서 생성한 flow이며 현재 연산자 전에 이들을 호출한다.
마찬가지로 현재 연산자 이후에 발생하는 모든 것들을 다운 스트림 flow라고 한다.
catch는 필요에 따라 에러를 발생시킬 수도 있고 새로운 값을 전송 시킬 수도 있다.
위 예시에서는 IllegalArgumentException 이 발생할 경우 Exception을 발생시키지만, 다른 예외가 발생하면 빈 리스트를 전달한다.
Observing Flows
이제 수집하는 방법에 대해 알아보자.
일반적으로 flow 수집은 화면에 데이터를 표시하기 위해 UI 계층에서 일어난다.
리스트에 최신 메시지를 표시한다는 가정하에 우리는 terminal 연산자를 사용해서 값을 수신하기 시작해야 한다.
스트림의 모든 값을 전송 즉시 가져오려면 collect를 사용하면 된다.
collect는 새로운 값이 생길 때마다 호출되는 함수를 파라미터로 받으며 suspend 연산자이기 때문에 코루틴 안에서 실행되어야 한다.
반면, intermediate(중간) 연산자는 일련의 연산자만 설정하며 아이템을 flow로 전송했을 때 간격을 두고 연산자를 실행한다.
위 코드에선 userMessages에서 collect를 호출할 때마다 새로운 flow가 생성되고 있다. 생산자 블록은 정해진 간격에 따라 API에서 메시지를 새로 고침할 것이다.
이런 flow를 코루틴 용어로 Cold Flow라고 한다. 이는 필요에 따라 생성되고 관찰되는 중에만 데이터를 전송하기 때문이다.
Flows in Android UI
이제 Android UI에서 최적의 방법으로 flow를 수집하는 방법을 알아보자.
고려해야 할 것은 다음과 같다.
- 앱이 백그라운드에 있을 때 리소스를 낭비하지 않는 것
- 구성 변경(Configuration Change)
MessageActivity에서 화면에 메시지 리스트를 표시해야 한다고 가정해보자.
flow가 얼마나 오래 수집되어야 할까? 화면에 UI가 표시되지 않을 대는 flow에서 수집을 중단해야 한다.
여기에 여러가지 옵션이 있는데, 모든 방식이 수명주기를 인지한다.
asLiveData flow 연산자는 flow를 LiveData로 변환해서 UI가 화면에 표시되는 동안에만 아이템을 관찰한다.
UI에서는 평소처럼 LiveData를 소비하기만 하면 된다.
약간의 편법을 사용해볼 수도 있다. UI 레이어에서 flow를 수집할 때 repeatOnLifecycle을 사용하는게 좋다. 이는 Lifecycle.State를 파라미터로 받는 suspend 함수이다. 이 API는 수명 주기를 인식하며 수명 주기가 해당 상태에 도달하면 블록을 전달할 새로운 코루틴이 자동으로 시작되고, 해당 수명 주기 아래로 떨어지면 취소가 된다.
여러 flow를 수집해야 할 경우, repeatOnlifecycle 블록에서 launch를 사용하여 여러 코루틴을 생성해야 한다.
또한 flowWithLifecycle 연산자는 수집할 flow가 하나뿐일 경우 repeatOnLifecycle 대신 사용할 수 있다.
이제 시각적으로 작동 원리를 확인해보자. 사용자가 홈 버튼을 눌렀을 때 백그라운드로 전송되고 액티비티에서 onStop이 호출된 다음, onStart가 호출되었을 때 다시 앱이 열린다.
이 때 STARTED 상태로 repeatOnLifecycle을 호출하면 UI가 화면에 표시되는 동안 flow 전송을 처리하고 앱이 백그라운드로 이동하면 수집이 취소된다.
repeatOnLifecycle과 flowWithLifecycle API는 lifecycle-runtime-ktx:2.4.0 라이브러리에 추가된 API이다.
위와 같은 방법으로 LifeCycle에 따라 관리를 하지 않으면 위험할 수 있다.
예를 들어 lifecycleScope에서 시작한 코루틴에서 바로 수집할 수도 있지만 이방식의 flow 수집은 위험하다.
왜냐하면 이는 백그라운드에 앱이 위치해도 flow에서 수집을 중단하지 않기 때문이다.
사실 위 방식 뿐만 아니라 LifecycleCoroutineScope.launchWhenX API에서도 비슷한 문제가 있다.
lifecycleScope.launch에서 flow를 수집하는 경우 액티비티가 백그라운드에 있을 때도 계속 flow가 업데이트 된다. 이는 낭비일 뿐 아니라 위험하기도 하다. 앱이 백그라운드일 때 다이얼로그를 표시하면 크래시를 일으킬 것이다.
이 문제를 해결하려면 onStart에서 수동으로 수집을 시작하고 onStop에서 수집을 중단해야 한다. 하지만 repeatOnLifecycle을 사용하면 이 같은 보일러 플레이트 코드를 제거할 수 있다.
이제 앱에서 구성 변경이 일어날 경우의 몇가지 요령을 알아보자.
flow를 뷰에 노출하면 수명 주기가 서로 다른 두 요소 사이에 데이터를 전달해야 한다는 걸 고려해야한다. 기기가 회전 되었거나 Configuration Change 이벤트를 수신하면 모든 액티비티를 다시 시작하지만 ViewModel은 그렇지 않다.
예를 들어 위와 같은 cold flow가 있다고 가정해보자. cold flow는 처음으로 수집될 때마다 다시 시작하기 때문에 레포지토리는 한 번 회전 후 다시 호출 될 것이다.
때문에 우리에겐 일종의 버퍼가 필요해진다. 데이터를 보관하고 있다가 여러 컬렉터 사이에 공유하면 된다. 재생성 횟수와는 상관없이 말이다.
StateFlow는 물로 비교하면 물탱크에 가깝다. 컬렉터가 없더라도 데이터를 보관하고 있다. 일회용 수집이 아닐 수 있으므로 액티비티나 프래그먼트와 함께 사용하는 것이 안전하다.
위의 예시에서 StateFlow의 여러 버전을 사용하며 필요할 때마다 이 값을 업데이트 할 수 있는 것을 볼 수 있다. 하지만 이것이 반응형이라고 보긴 어려울 것이다.
우리는 flow를 StateFlow로 변환하여 리르 개선할 수 있다. 이렇게 하면 StateFlow가 업스트림 flow에서 모든 업데이트를 받아서 최신 값을 저장하게 된다. 콜렉터가 많거나 없을 수 있으므로 VIewModel에서 사용하기에 적당하다.
여러 타입의 flow가 있지만 StateFlow를 매우 정확하게 최적화할 수 있으므로 이 방법을 권장한다.
flow를 StateFlow로 변환할 때, stateIn 연산자를 함께 사용할 수 있다.
initialValue는 값이 항상 들어있어야 하기 때문에 사용할 수 있으며 scope는 코루틴 공유가 시작되는 시점을 제어하는데 ViewModelScope를 사용할 수 있다.
started는 흥미로운 파라미터이다. WhileSubscribed(5000)의 의미를 알아보기 위해 먼저 두 가지 케이스를 살펴보자.
![](https://blog.kakaocdn.net/dn/d8hTEC/btsmj7qNNG3/einU9MRTAJj4Qfdlb2Wkok/img.png)
첫 번째 예시는 flow의 컬렉터인 액티비티가 일정 시간 파괴되었다가 다시 생성되는 화면 회전이다..
![](https://blog.kakaocdn.net/dn/JdPKX/btsmkM0ICFD/s9e4eq7eec7ZFhyAqUYLZk/img.png)
두 번째 예시는 홈으로 이동해서 앱을 백그라운드로 보내는 것이다.
![](https://blog.kakaocdn.net/dn/clrmbZ/btsmkt799AT/cKb2URSwjIkjSWgpUidAo0/img.png)
회전 시나리오에서는 최대한 빠르게 전환하기 위해 flow를 다시 시작해서는 안된다. 하지만 두 번째 시나리오에서는 배터리와 다른 리소스를 아끼기 위해 모든 flow를 중단해야 한다. 그렇다면 어떤 시나리오인지 어떻게 감지할 수 있을 까?
바로 시간 초과이다.
StateFlow의 수집이 중단되었을 때 모든 업스트림 flow를 중단하진 않는다. 오히려 가령 약 5초 정도 잠시 기다린다. 시간 초과 전에 flow를 수집하면 업스트림 flow가 취소되지 않는다.
WhileSubscribed(5000)은 바로 그런 일을 하는 파라미터이다.
![](https://blog.kakaocdn.net/dn/cDp2Lw/btsmkv5XNnS/uRXttWuFmYxhARhfLPmVEk/img.png)
위 그림에서 앱이 백그라운드에 갔을 때 반응을 시각적으로 확인해볼 수 있다. 홈 버튼을 누르기 전에 뷰가 업데이트를 수신하고 StateFlow는 정상적으로 업데이트 flow를 생성한다.
![](https://blog.kakaocdn.net/dn/ce7AMj/btsmiEvC0Z5/vkoZhOdraGga1JI3iQhjGk/img.png)
이제 뷰가 중단되면 수집이 즉시 종료된다. 그러나 StateFlow는 우리가 설정한 옵션으로 인해 업스트림 flow를 중단하는데 5초가 걸린다. 그리고, 제한 시간이 지나 업스트림 flow가 취소된다.
![](https://blog.kakaocdn.net/dn/P3jSJ/btsmiG74Yr5/HAMymmJKk9zq7sOvAnW46k/img.png)
사용자가 앱을 다시 열 경우 업스트림 flow가 자동으로 다시 시작된다.
![](https://blog.kakaocdn.net/dn/ScdAh/btsmd2DxM3a/u9DUgTbhaYex0F6KRm4NYk/img.png)
하지만 회전 시나리오에서 뷰는 잠시 중단된다.. 따라서 StateFlow는 절대 복원되지 않고 모든 업스트림 flow를 활성상태로 유지하며 아무 일도 없었던 것처럼 사용자에게 회전 인스턴스를 보낸다.
즉 StateFlow를 사용하여 ViewModel에서 flow를 노출하거나 asLiveData를 사용하여 이와 동일한 작업을 수행하는 것이 좋다.
출처
'Android > Flow' 카테고리의 다른 글
[Android] 플로우(Flow)의 Operator (0) | 2025.02.15 |
---|---|
[Android] 플로우(Flow)란? - Deep Dive (0) | 2025.02.15 |
Coroutine Flow(3) LiveData vs StateFlow (0) | 2023.07.03 |
Coroutine Flow(2) - StateFlow, SharedFlow (0) | 2023.06.30 |
Coroutine Flow(1) - Flow 란 (0) | 2023.06.27 |