- 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다. 👍
- Android Dev Summit 2021 동영상 중 Kotlin Flows in practice 동영상을 공부하며 이해하기 쉽게 한글 문맥으로 더 자연스럽게 번역 및 정리한 글입니다.
2편에서 살펴본 repeatOnLifecycle과 flowWithLifecycle API 방법 외에도 다른 방법으로 플로우를 수집할 수도 있습니다.
예를 들어, lifecycleScope에서 시작된 코루틴에서 바로 수집할 수도 있습니다.
하지만 이런 방식의 플로우 수집은 위험할 수 있습니다. 위의 코드는 플로우를 수집하고 UI를 업데이트하도록 동작하지만, 문제는 백그라운드에서도 앱이 멈추지않고 계속하여 플로우를 수집한다는 것입니다.
리소스를 낭비하지 않기 위해서는 데이터가 화면에 표시되지 않을 때, 플로우에서 수집을 계속해서는 안됩니다.
위에서 살펴본 API만 그런건 아니고, LifecycleCoroutineScope.launchWhenX API 에도 비슷한 문제가 있습니다.
lifecycleScope.launch에서 직접 수집하는 경우, Activity가 백그라운드에 있을 때에도 계속해서 플로우 업데이트를 받습니다.
이는 리소스 낭비일 뿐만 아니라 앱이 위험한 상태에 빠지기도 합니다. 예를 들어 어떤 플로우를 업데이트받고, 그 업데이트에 따른 이벤트로 Dialog를 출력한다면, 앱이 백그라운드일 때 UI 변경이 일어나면서 앱 크래쉬가 발생합니다.
이 문제를 해결하려면 onStart 에서 수동으로 수집을 시작하고, onStop에서 수집을 중단해야 합니다. 매번 이러한 처리를 하는 게 아주 번거롭겠죠? 하지만 괜찮습니다.
repeatOnLifecycle을 사용하면 이러한 작업을 수행하면서, 동시에 보일러플레이트 코드를 제거하여 편하게 사용할 수 있습니다.
launchWhenStarted를 대안으로 고려할 경우, lifecycleScope.launch보다는 나은 편입니다.
앱이 백그라운드에 있을 때 플로우 수집을 중단하기 때문이죠. 하지만 이 방법은 UI Layer에서 플로우 수집만 중단할 뿐, 생산자는 계속 활성화되어있는 상태이기 때문에 생산자는 화면에 표시되지 않을 항목을 계속해서 플로우를 업데이트(emit)하면서 메모리를 낭비하게 될 수 있습니다.
위에서 살펴본 이유로 대부분의 경우 repeatOnLifecycle이나 flowWithLifecycle을 사용하는 것이 안전합니다. 이 API들은 UI가 백그라운드에 있을 때 플로우를 수집하지 않고, 업스트림 플로우도 중지됩니다. 그러면서도 플로우 생산자들은 Active 하게 둡니다.
이제 앱에서 구성을 변경할 때의 몇 가지 요령을 살펴보겠습니다.
View에 플로우를 보여줄 때, 라이프 사이클이 서로 다른 두 요소 사이에 데이터를 전달해야 한다는 것을 고려해야합니다. Activity와 Fragment 라이프 사이클은 까다로울 수 있습니다. 한 가지 중요한 예시로 설명해 보겠습니다.
디바이스가 회전되거나 구성 변경이 수신되면, 모든 Activity들은 다시 시작하지만 ViewModel은 그렇지 않습니다.
ViewModel에서 모든 플로우를 노출하는 건 아닙니다. 이런 콜드 플로우를 그런 예시로 들 수 있습니다.
콜드 플로우는 처음으로 수집될 때마다 다시 시작하기 때문에, 리포지토리는 회전 후에 다시 호출됩니다.
우리는 여기서 일종의 버퍼(buffer)가 필요합니다. 데이터를 보관하고 있다가 여러 컬렉터들에게 이를 공유해 주는 것이죠. 이렇게 하면 몇 번이든 재생성돼도 상관이 없습니다. StateFlow가 정확히 이런 목적으로 생성되었습니다.
StateFlow는 물로 비유하면 물탱크라고 할 수 있습니다. 컬렉터가 아무도 없더라도 데이터를 보관합니다. Activity 또는 Fragment와 StateFlow를 함께 사용해서 이 데이터를 담아놓고 있는 탱크(StateFlow)로부터 여러 번 플로우를 수집할 수 있습니다.
위 예시의 코루틴처럼 StateFlow의 mutable version 인 MutableStateFlow을 이용해서 원할 때마다 해당 StateFlow의 값을 업데이트할 수 있습니다. UiState를 담아두는 MutableStateFlow에다가 처음엔 Result.Loading을 값으로 설정해 줬다가, 이후에 repository를 통해 데이터를 받아와서 해당 값으로 다시 설정해 줍니다. 근데 이러한 방식은 Reactive 하지 않습니다. 개선을 해야 되겠죠.
이러한 방식 대신에 플로우를 StateFlow로 변환할 수 있습니다. 이렇게 하면 StateFlow가 업스트림 플로우에서 모든 업데이트를 받아서, 최신 값을 저장합니다. 컬렉터가 없거나 많아도 상관없으므로 ViewModel에서 사용하기에 최적입니다.
여러 유형의 플로우가 있지만, 매우 정확하게 최적화할 수 있는 StateFlow를 권장합니다.
플로우를 StateFlow를 변환할 때 stateIn 연산자(operator)를 사용할 수 있습니다. 이 함수는 3가지 파라미터를 받습니다.
1. initialValue - StateFlow의 초기값입니다. StateFlow는 항상 value가 있어야 합니다.
2. scope - 플로우의 공유가 시작되는 시점을 제어합니다. 여기에 ViewModel의 Scope를 사용할 수 있습니다.
3. started - 이 값은 꽤나 흥미로운 항목입니다. 아래에서 WhileSubscribed(5000)이 무슨 의미인지 살펴보겠습니다.
이를 위해서 먼저 두 가지 시나리오를 설명하겠습니다.
첫째, 플로우의 컬렉터인 Activity가 회전하는 동안 잠깐동안 파괴되었다가 다시 재생성되는 시나리오입니다.
둘째, 홈으로 이동해서 앱이 백그라운드 상태가 되는 시나리오입니다.
첫째 회전 시나리오에서는 최대한 빠르게 전환하기 위해서 플로우를 다시 시작해서는 안 됩니다. 기존에 있는 데이터를 그냥 쓰면 더 빠르게 화면을 그릴 수 있고, 그리고 화면 전환 후에 어차피 UI는 계속해서 플로우를 수집해야 합니다. 그러나 홈으로 이동 는 경우에는, 배터리와 다른 리소스를 아끼기 위해 모든 플로우를 중단해야 합니다. 그러면 어떤 시나리오인지 어떻게 탐지할 수 있을까요?
Timeout을 통해 가능합니다. StateFlow의 수집이 중단되었을 때, 모든 업스트림 플로우를 즉시 중단하는 대신 잠시동안 기다립니다. 예를 들면 위에서 설정한 5000ms(5초) 처럼요. 설정한 Timeout 전에 플로우를 수집하면 업스트림 플로우는 취소되지 않습니다. whileSubscribed(5000)이 바로 이런 역할을 합니다.
위 표에서는 앱이 백그라운드에 들어갔을 때 어떻게 진행되는지를 보여줍니다.
홈 버튼을 누르기 전에는, View가 업데이트를 수신하고 StateFlow는 생성되고 있는 업스트림 플로우를 가지고 있습니다.
앱이 백그라운드에 들어가서 View가 중단되면, View에서의 플로우 수집이 즉시 종료됩니다..
그러나 StateFlow는 whileSubscribed(5000) 선언돼있으므로, 수집이 종료된 이후에도 업스트림 플로우를 제한 시간 5초 동안 유지한 뒤에, 그 이후에도 수집이 없으면 그때서야 업스트림 플로우를 취소합니다.
이후 사용자가 앱을 다시 열 때 Activity가 onStart에 진입하면서부터 업스트림 플로우는 자동으로 다시 시작됩니다.
그러나 회전 시나리오에서는 View는 아주 잠깐 중단됩니다. 5초 이내이죠. 따라서 StateFlow는 모든 업스트림 플로우를 활성 상태로 유지하며, 아무 일도 없었던 것처럼 사용자에게 회전된 UI를 보여줍니다.
요약하자면, StateFlow를 사용하여 ViewModel의 플로우를 노출하거나, asLiveData를 사용해서 이와 동일한 작업을 실행하는 것을 추천합니다.
'Android' 카테고리의 다른 글
[Android] Kotlin Flows in practice 번역 및 정리 (2) (0) | 2023.04.22 |
---|---|
[Android] Kotlin Flows in practice 번역 및 정리 (1) (0) | 2023.04.21 |
[Android] 앱 아키텍처 가이드 (2) (0) | 2023.04.15 |
[Android] 앱 아키텍처 가이드 (1) (0) | 2023.04.15 |
[Android] 사용자 데이터 백업 (자동 백업) (0) | 2023.04.13 |