본 글은 Jose Alcérreca님의 글을 한국어로 번역한 글입니다. 원문 링크 👇
LiveData는 2017년 당시 우리에게 필요한 것이었습니다. 옵저버 패턴은 우리의 개발을 더 쉽게 해주었지만, Rxjava와 같은 옵션은 그 당시 초보 개발자들에게는 너무 복잡했습니다. 그래서 Architecture Component 팀은 Android 용으로 설계된 옵저버블한 데이터 홀더 클래스인 LiveData를 만들었습니다. 이것은 쉽게 사용할 수 있도록 간단하게 만들었고 리액티브 스트림의 경우엔 RxJava를 사용할 것을 권고하며 두 가지를 통합적으로 활용하는 방법을 제시했습니다.
DeadData?
LiveData는 여전히 Java 개발자, 초심자 및 간단한 상황을 위한 좋은 솔루션입니다. 하지만 그 외의 나머지는 Kotlin의 Flow로 이동하는 것이 좋은 방법입니다. Flow는 가파른 러닝 커브를 가지고 있지만 Jetbrains에 의해 지원되는 Kotlin 언어의 일부입니다. 그리고 리액티브 모델에 잘 맞는 Compose가 출시되고 있습니다.
한 동안 Flow를 사용하여 View 및 ViewModel을 제외한 앱의 여러 부분을 연결하는 것에 대해 이야기했습니다. 이제 더 안전하게 Android UI에서 Flow를 수집할 수 있는 방법이 존재하므로 완전한 마이그레이션 가이드를 만들 수 있게 되었습니다.
이 게시물에서는 Flow를 View에 노출하는 방법, 수집 방법 및 특정 상황에 맞게 Flow를 미세하게 조정하는 방법에 대해 알아봅니다.
Flow: 간단한 것은 어렵고 복잡한 것은 쉽다
LiveData는 데이터를 노출하는 동시에 최신 값을 캐싱하고 Android의 라이프사이클을 이해하는 작업을 수행합니다. 나중에 우리는 그것이 코루틴을 시작하고 나서 복잡한 변형을 발생시킬 수 있다는 것을 알게 되었습니다.
LiveData는 데이터를 노출하는 동시에 최신 가치를 캐슁하고 Android의 라이프사이클을 이해한다는 한 가지 작업을 수행했습니다. 나중에 우리는 그것이 코루틴을 시작하고 복잡한 변형을 일으킬 수 있다는 것을 알게 되었습니다. 하지만 이것은 조금 더 관련이 있었습니다.
몇 가지 LiveData 패턴과 동등한 Flow에 대해 살펴보겠습니다.
#1: Mutable 데이터 홀더로 one-shot 작업 결과 노출
이것은 코루틴의 결과로 state 홀더를 변경시키는 클래식한 패턴입니다.
이제 동일하게 우리는 Flow의 (Mutable)StateFlow를 사용합니다.
StateFlow는 SharedFlow(Flow의 특수 타입)의 특별한 종류이며 LiveData에 가장 가깝습니다.
- 이것은 항상 값을 가지고 있습니다.
- 이것은 오직 한 가지 값을 가집니다.
- 이것은 여러 개의 observer를 지원합니다.(flow가 공유됨)
- 이것은 observer 수에 관계없이 항상 구독하는 것의 최신 값을 받습니다.
UI 상태를 View에 노출시킬 때는 StateFlow를 사용합니다. UI 상태를 유지하도록 설계된 안전하고 효율적인 옵저버입니다.
#2: one-shot 작업 결과 노출
이것은 이전의 코드와 동일하며, 변경 가능한 backing property없이 코루틴 호출의 결과를 노출시킵니다.
LiveData에서는 다음과 같은 용도로 liveData
코루틴 빌더를 사용했습니다.
state 홀더는 항상 값을 가지고 있으므로, Loading
, Success
, Error
와 같은 상태를 지원하는 Result
클래스에서 UI 상태를 래핑하는 것이 좋습니다.
다음과 같은 몇 가지 구성을 작성해야 하므로 Flow 코드가 약간 더 많이 포함됩니다.
stateIn은 Flow를 StateFlow로 변환하는 Flow 연산자 입니다. 파라미터에 대한 설명을 하려면 복잡해지므로 일단 이러한 파라미터를 신뢰합시다.
#3: 파라미터가 포함된 one-shot 데이터 로드
사용자의 ID에 따라 달라지는 일부 데이터를 로드하려고 하면 Flow를 노출하는 AuthManager
에서 이 정보를 얻을 수 있다고 가정해봅시다.
LiveData를 사용하면 우리는 이런 작업을 수행할 것입니다.
switchMap
은 userId
가 변경될 때 body 블록이 실행되고 결과가 구독되는 변환입니다.
userId
가 LiveData일 이유가 없는 경우 스트림을 Flow와 결합하여 마지막으로 노출된 결과를 LiveData로 변환하는 것이 더 나은 방법입니다.
Flow와 위 작업은 매우 유사합니다.
조금 더 유연성이 필요한 경우 transformLatest
를 사용하여 아이템을 명시적으로 emit
할 수 있습니다.
#4: 매개 변수를 사용하여 데이터 흐름 관찰
이제 이 예시를 사용하여 조금 더 반응적으로 만들어 보겠습니다. 데이터는 가져오진 않지만 관찰되므로 데이터 원본의 변경 내용을 UI에 자동으로 전파합니다.
우리의 예제를 계속해봅시다: 데이터 원본에서 fetchItem
을 호출하는 대신, 우리는 Flow를 반환하는 가상의 observeItem
기능을 사용합니다.
LiveData를 사용하면 flow를 LiveData로 변환하고 모든 업데이트를 emitSource
를 사용하여 내보낼 수 있습니다.
또한, 가급적이면 flatMapLatest
를 사용하여 두 개의 flow를 결합하고 출력만 LiveData로 변환하는 것이 좋습니다.
Flow의 구현은 위와 유사하지만 LiveData로 변환하는 부분이 없습니다.
View에 노출되는 StateFlow는 사용자가 변경되거나 리포지토리의 사용자 데이터가 변경될 때마다 해당 업데이트를 수신하게 됩니다.
#5 여러 소스의 결합: MediatorLiveData -> Flow.Combine
MediatorLiveData를 사용하면 하나 이상의 업데이트 소스(관찰 가능한 LiveData)를 관찰하고 새로운 데이터를 가져올 때 어떤 작업을 수행할 수 있습니다. 일반적으로 MediatorLiveData의 값을 업데이트하여 사용합니다.
Flow에서는 훨씬 더 간단합니다.
combineTransform 이나 zip을 사용할 수도 있습니다.
노출된 StateFlow 구성(stateIn 연산자)
이전에 사용했던 stateIn
은 일반적인 flow를 StateFlow로 변환하지만, 구성에 필요한 코드가 요구됩니다. 지금 바로 사용해야하고 자세한 설명이 필요하지 않은 경우 이 조합을 사용하는 것이 좋습니다.
https://gist.github.com/JoseAlcerreca/aa59b54c5c40e2843e49427d1d3c40f9#file-myviewmodel-kt
그러나 started 파라미터가 5초가 확실하지 않다면 아래 사항을 읽어보세요.
stateIn에는 다음과 같은 3개의 파라미터가 있습니다.(docs)
@param scope the coroutine scope in which sharing is started.
@param started the strategy that controls when sharing is started and stopped.
@param initialValue the initial value of the state flow.
This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with the `replayExpirationMillis` parameter.// scope -> 공유가 시작되는 코루틴 스코프
// started -> 공유가 시작 및 중지 시기를 제어하는 전략을 시작할 수 있는 파라미터
// initialValie -> state flow의 초기 값. 이 값은 [SharingStarted.WhileSubscribed]과 `replayExpirationMillis` 파라미터를 사용하여 state flow가 재설정 되는 경우에도 사용됩니다.
started
는 세 가지 값을 가질 수 있습니다.
Lazily
: 첫 번째 subscriber가 나타나면 시작하고scope
가 취소되면 중지합니다.Eagerly
: 즉시 시작되며scope
가 취소되면 중단됩니다.WhileSubscribed
: 이것은 복잡합니다.
one-shot 작업에는 Lazily
또는 Eagerly
를 사용할 수 있습니다. 그러나 이 외의 flow를 관찰하는 경우에는 아래에 설명된 대로 중대한 최적화 작업을 수행하려면 WhileSubscribed
를 사용해야합니다.
WhileSubscribed 전략
WhileSubscribed는 collector가 없을 때 upstream flow를 취소합니다. stateIn
을 사용하여 생성된 StateFlow는 데이터를 View에 표시하지만 다른 레이어 또는 애플리케이션(upstream)에서 나오는 flow도 관찰합니다. 이러한 flow를 활성화된 상태로 유지하면 (예를 들어 데이터베이스 연결, 하드웨어 센서 등과 같은 다른 소스에서 데이터를 계속 읽는 경우) 리소스가 낭비될 수 있습니다. 앱이 백그라운드로 전환되면 이러한 코루틴을 중단해야 합니다.
WhileSubscribed
는 두 가지 파라미터를 가집니다.
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
Stop timeout
문서에 의하면:
stopTimeoutMillis
는 마지막 subscriber가 사라짐과 upstream flow의 중지 사이의 지연 시간을 구성합니다. 기본 값은 0입니다. (즉시 중지)
이 기능은 view가 몇 초 동안 수신을 중지한 경우 upstream flow를 취소하지 않으려는 경우에 유용합니다. 예를 들어 사용자가 장치를 회전하고 view가 연속적으로 삭제 및 재생성 되는 경우, 이 문제는 항상 발생합니다.
liveData 코루틴 빌더의 솔루션은 구독자가 없는 경우 코루틴이 중지되는 5초의 지연 시간을 추가하는 것이었습니다. WhileSubscribed(5000)
은 동일한 작업을 수행합니다.
이 방법은 모든 요구사항을 충족합니다.
- 사용자가 앱을 백그라운드로 보내면 다른 레이어에서 제공되는 업데이트가 5초 후에 중지되어 배터리가 절약됩니다.
- 최신 값은 여전히 캐시되므로 사용자가 다시 접근할 때 View에 즉시 일부 데이터가 포함됩니다.
- 구독이 다시 시작되고 새 값이 들어온 후 사용 가능할 때 화면이 새로 리프레시됩니다.
Replay 만료
오래된 데이터가 너무 오랫동안 사라졌을 때 유저에게 오래된 데이터가 표시되지 않도록 하고 로딩 화면을 표시하려면 WhileSubscribed
에서 replayExpirationMillis
파라미터를 체크하세요. 이 경우 캐시된 값이 stateIn
에 정의된 초기 값으로 복원되므로 매우 편리하고 약간의 메모리도 절약할 수 있습니다. 앱으로 돌아오는 것은 그렇게 빠르지 않겠지만, 오래도니 데이터는 보여주지 않을 것 입니다.
replayExpirationMillis
— 공유하는 코루틴의 중지와 replay 캐시 재설정 사이의 지연 시간을 구성합니다. (shareIn
연산자의 캐시를 비우고 캐시된 값을stateIn
연산자에 대한 원래initialValue
로 재설정) 기본값은Long.MAX_VALUE
입니다. (replay 캐시를 영구적으로 유지, 버퍼를 재설정하지 않음) 0값을 사용하여 캐시를 즉각적으로 만료시킬 수 있습니다.
view에서 StateFlow 관찰
지금까지 살펴본 바와 같이, ViewModel의 StateFlow가 더 이상 listening을 하지 않음을 view에게 알려주는 것이 매우 중요합니다. 그러나 lifecycle과 관련된 모든 것이 그렇듯이 그리 간단하지 않습니다. flow를 수집하려면 코루틴이 필요합니다. 그래서 Activity 및 Fragment는 많은 코루틴 빌더를 제공합니다.
Activity.lifecycleScope.launch
: 코루틴을 즉시 시작하고 activity가 파괴되면 코루틴을 취소합니다.Fragment.lifecycleScope.launch
: 코루틴을 즉시 시작하고 fragment가 파괴되면 취소합니다.Fragment.viewLifecycleOwner.lifecycleScope.launch
: 코루틴을 즉시 시작하고 fragment의viewLifecycle
이 파괴되면 취소합니다. UI를 수정하는 경우viewLifecycle
을 사용해야합니다.
LaunchWhenStarted, launchWhenResumed…
특수한 버전의 launch
로 불리는 launchWhenX
는 lifecycleOwner
가 X 상태가 될 때까지 기다렸다가 lifecycleOwner
가 X 상태 아래로 떨어질 때 코루틴을 일시 중단합니다. 주목해야할 것은 lifecycle owner가 파괴될 때까지 코루틴을 취소하지 않는다는 것입니다.
앱이 백그라운드에 있는 동안 업데이트를 수신하면 크래시가 발생할 수 있으며, 이 크래시는 view에서 flow를 일시 중단하여 해결할 수 있습니다. 하지만 upstream flow는 앱이 백그라운드에 있는 동안 활성화된 상태를 유지하므로 리소스를 낭비할 수 있습니다.
이는 지금까지 StateFlow를 사용하기 위해 해온 모든 작업이 무용지물이 되리라는 것을 의미합니다. 하지만, 이를 위한 새로운 API가 있습니다.
lifecycle.repeatOnLifecycle을사용하여 해결
이 새로운 코루틴 빌더는 우리가 필요로 하는 것을 정확히 수행합니다. 코루틴은 특정 상태에서 코루틴을 시작하고 lifecycle owner가 그 아래 상태로 떨어질 때 코루틴을 중단합니다.
Fragment에서 사용예시입니다.
fragment의 view가 STARTED 상태가 되면 수집이 시작되고, RESUME 상태에서 다시 재개되며, STOPPED 상태에서 중지됩니다. 자세한 내용은 Android UI에서 flow를 보다 안전하게 수집할 수 있는 방법을 참조하세요.
위의 StateFlow 지침과 repeatOnLifecyle
API를 함께 사용하면 기기의 리소스를 잘 활용하는 동시에 최상의 성능을 얻을 수 있습니다.
주의 : 최근 Data Binding에 추가된 StateFlow 지원에서는 업데이트를 수집하기 위해
launchWhenCreated
를 사용하며, 안정화 단계에 들어선 후,repeatOnLifecycle
을 사용하게 됩니다.Data Binding의 경우 어디에서나 flow를 사용하고
asLiveData
를 추가하면 view에 데이터가 노출됩니다.lifecycle-runtime-ktx 2.4.0
이 안정적 단계에 들어서면 Data Binding이 업데이트 될 것입니다.
요약
ViewModel에서 데이터를 노출하고 View에서 수집하는 가장 좋은 방법은 다음과 같습니다.
- ✔️
WhileSubscribed
전략을 통한 시간 초과를 사용하여 StateFlow를 노출합니다. [예시] - ✔️️️
repeatOnLifecycle
을 사용하여 수집합니다. [예시]
다음 조합은 upstream flow를 계속 활성화하여 리소스를 낭비합니다.
- ❌
WhileSubscribed
을 사용하고lifecycle.launch
/launchWhenX
내부에서 수집합니다. - ❌
Lazily
/Eagerly
을 사용하고repeatOnLifecycle
로 수집합니다.
물론 Flow의 모든 기능이 필요하지 않다면 LiveData를 사용해도 충분합니다:)