Android/Flow

[Android] 안드로이드 환경의 Flow Lifecycle

태크민 2025. 2. 17. 18:59

 

FlowLiveData처럼 android에서 lifecycle에 따라 자동으로 멈추거나 재시작되지 않습니다. 따라서 위에서 사용했던 예제의 경우 home키로 화면을 나가더라도 종료되지 않고 계속해서 emit 되는 값을 collect 합니다.

override fun onCreate(savedInstanceState: Bundle?) {
        ... 
    MainScope().launch {
        testViewModel.connectionFlow.collect {
            Log.i(TAG, "connectionFlow: $it")
        }
    }
        ...
}

 

Flows in Android UI

Android UI에서 최적의 방법으로 flow를 수집하는 방법을 알아보겠습니다.

고려해야 할 것은 다음과 같습니다.

  • 앱이 백그라운드에 있을 때 리소스를 낭비하지 않는 것
  • 구성 변경 (Configuration Change)

MessagesActivity에서 화면에 메시지 리스트를 표시해야한다고 가정해봅시다.

 

flow가 얼마나 오래 수집되어야 할까요? 화면에 UI가 표시되지 않을 때는 flow에서 수집을 중단해야 합니다.

 

여기에 여러 가지 옵션( (asLiveData, repeatOnLifecycle, flowWithLifecycle) 이 있는데, 모든 방식이 수명 주기를 인지합니다.

 

 

asLiveData flow 연산자는 flow를 LiveData로 변환해서 UI가 화면에 표시되는 동안에만 아이템을 관찰합니다.

 

UI에서는 평소처럼 LiveData를 소비하기만 하면 됩니다.

 

 

약간의 편법을 사용해 볼 수도 있습니다. UI 레이어에서 flow를 수집할 때 repeatOnLifecycle을 사용하는게 좋습니다. 이는 Lifecycle.State를 파라미터로 받는 suspend 함수입니다. 이 API는 수명 주기를 인식하며 수명 주기가 해당 상태에 도달하면 블록을 전달할 새로운 코루틴이 자동으로 시작되고, 해당 수명 주기 아래로 떨어지면 취소됩니다.

 

이 API는 수명 주기가 초기화 될 때 (onCreate) 호출하는 것이 좋습니다. 또한 수명 주기가 파괴될 때까지(onDestroy) 실행을 다시 시작하지 않습니다.

 

 

여러 flow를 수집해야 할 경우, repeatOnLifecycle 블록에서 launch를 사용하여 여러 코루틴을 생성해야 합니다.

 

 

또한 flowWithLifecycle 연산자는 수집할 flow가 하나뿐일 경우 repeatOnLifecycle 대신 사용할 수 있습니다.

 

이제 시각적으로 작동 원리를 확인해보겠습니다. 사용자가 홈 버튼을 눌렀을 때 백그라운드로 전송되고 액티비티에서 onStop이 호출된 다음, onStart가 호출되었을 때 다시 앱이 열립니다.

이 때 STARTED 상태로 repeatOnLifecycle을 호출하면 UI가 화면에 표시되는 동안 flow 전송을 처리하고 앱이 백그라운드로 이동하면 수집이 취소됩니다.

즉, activity가 onstart 되는 시점에 새로운 coroutine으로 수집이 시작되며, onStop시점에 cancel 됩니다.

 

repeatOnLifecycle과 flowWithLifecycle API는 lifecycle-runtime-ktx:2.4.0 라이브러리에 추가된 API입니다.
 

이는 신규 API이므로 다른 방식으로 UI에서 flow를 수집할 수도 있습니다.

 

예를 들어 lifecycleScope에서 시작한 코루틴에서 바로 수집할 수도 있습니다. 이 방식의 flow 수집은 위험할 수 있습니다. 이는 백그라운드에 앱이 위치해도 flow에서 수집을 중단하지 않습니다.

 

사실 위 방식 뿐만 아니라 LifecycleCoroutineScope.launchWhenX API에서도 비슷한 문제가 있습니다. (현재는 Deprecated)

 

 

lifecycleScope.launch에서 flow를 수집하는 경우 액티비티가 백그라운드에 있을 때도 계속 flow가 업데이트 됩니다. 

이는 낭비일 뿐 아니라 위험하기도 합니다. 앱이 백그라운드일 때 다이얼로그를 표시하면 크래시를 일으킬 것입니다.

 

 

문제를 해결하려면 onStart에서 수동으로 수집을 시작하고 onStop에서 수집을 취소해야 합니다. repeatOnLifecycle을 사용하면 이 같은 보일러플레이트 코드를 제거할 수 있습니다.

 

launchWhenStarted를 대안으로 고려할 경우엔 lifecycleScope.launch보단 나은 편입니다. 왜냐면 앱이 백그라운드에 있을 때 flow 수집을 중단하기 때문입니다.

하지만 이 방식은 Flow가 계속 활성화된 상태로 남아있는 문제가 있습니다.
이는 launchWhenStarted가 repeatOnLifecycle과 다르게 백그라운드로 전환될 때 코루틴을 완전히 취소(cancel)하는 것이 아니라 단순히 일시 중단(suspend)하는 방식이기 때문입니다.

즉, launchWhenStarted를 사용할 경우 불필요한 메모리 점유와 리소스를 낭비하는 문제가 있습니다.

 

 

따라서, repeatOnLifecycle이나 flowWithLifecycle을 안전하게 사용하는 것을 권장합니다. flowWithLifecycle는 내부적으로 repeatOnLifecycle API를 사용하며, Lifecycle이 지정된 상태로 진입할 때만Flow의 아이템을 방출 및 수집하고, 상태를 벗어나면 작업을 취소하는 특징을 가지고 있습니다.

 

정리하자면, repeatOnLifecycle은 매개변수로 전달된 Lifecycle.State가 해당 state에 도달하면 블록에 있는 새 코루틴을 자동으로 생성 및 시작하고, lifecycle이 state 아래로 떨어질 때 블록 안에 실행중인 코루틴을 취소하는 역할을 가진 suspend함수입니다.

 

이제 앱에서 구성 변경이 일어날 경우의 몇 가지 요령을 알아봅시다.

 

 

flow를 뷰에 노출하면 수명 주기가 서로 다른 두 요소 사이에 데이터를 전달해야 한다는 걸 고려해야 합니다. 특히 프래그먼트에서 까다로울 수 있습니다.

기기가 회전되었거나 Configuration Change 이벤트를 수신하면 모든 액티비티를 다시 시작하지만 ViewModel은 그렇지 않습니다.

 

그래서 ViewModel에서 모든 flow를 노출하는 것은 아닙니다. 예를 들어 위와 같은 cold flow가 있다고 가정해봅시다. cold flow는 처음으로 수집될 때마다 다시 시작하기 때문에 레포지토리는 한 번 회전 후 다시 호출될 것입니다.

 

때문에 우리에겐 일종의 버퍼가 필요해집니다. 데이터를 보관하고 있다가 여러 컬렉터 사이에 공유하면 됩니다. 재생성 횟수와는 상관없이 말입니다.

 

 

StateFlow는 바로 이런 목적으로 만들어졌습니다.

 

 

StateFlow는 물로 비유하면 물탱크에 가깝습니다. 컬렉터가 없더라도 데이터를 보관하고 있습니다. 일회용 수집이 아닐 수 있으므로 액티비티나 프래그먼트와 함께 사용하는게 안전합니다.

위의 예시에서 StateFlow의 여러 버전을 사용하며 필요할 때마다 값을 업데이트 할 수 있는 것을 볼 수 있습니다. 하지만 이것이 반응형이라고 보긴 어려울 것입니다.

 

우리는 flow를 StateFlow로 변환하여 이를 개선할 수 있습니다. 이렇게 하면 StateFlow가 업스트림 flow에서 모든 업데이트를 받아서 최신 값을 저장하게 됩니다. 콜렉터가 많거나 없을 수 있으므로 ViewModel에서 사용하기에 적당합니다.

 

여러 타입의 flow가 있지만 StateFlow를 매우 정확하게 최적화할 수 있으므로 이 방법을 권장드립니다.

 

 

flow를 StateFlow로 변환할 때, stateIn 연산자를 함께 사용할 수 있습니다. initialValue는 값이 항상 들어있어야 하기 때문에 사용할 수 있으며 scope는 코루틴 공유가 시작되는 시점을 제어하는데 viewModelScope를 사용할 수 있습니다.

started는 흥미로운 파라미터입니다. WhileSubscribed(5000)의 의미를 알아보기 위해 먼저 두 가지 케이스를 살펴보겠습니다.

 

 

첫 번째 예시는 flow의 컬렉터인 액티비티가 일정 시간 파괴되었다가 다시 생성되는 화면 회전입니다.

 

 

두 번째 예시는 홈으로 이동해서 앱을 백그라운드로 보내는 것입니다.

 

 

회전 시나리오에서는 최대한 빠르게 전환하기 위해 flow를 다시 시작해서는 안됩니다. 하지만 두 번째 시나리오에서는 배터리와 다른 리소스를 아끼기 위해 모든 flow를 취소해야 합니다. 그렇다면 어떤 시나리오인지 어떻게 감지할 수 있을 까요?

바로 시간 초과입니다.

 

StateFlow의 수집이 중단되었을 때 모든 업스트림 flow를 중단하진 않습니다. 오히려 가령 약 5초 정도 잠시 기다립니다. 시간 초과 전에 flow를 수집하면 업스트림 flow가 취소되지 않습니다.

 

WhileSubscribed(5000)은 바로 그런 일을 하는 파라미터입니다.

 

위 그림에서 앱이 백그라운드에 갔을 때 반응을 시각적으로 확인해볼 수 있습니다. 홈 버튼을 누르기 전에 뷰가 업데이트를 수신하고 StateFlow는 정상적으로 업데이트 flow를 생성합니다.

 

이제 뷰가 중단되면 수집이 즉시 종료됩니다. 그러나 StateFlow는 우리가 설정한 옵션으로 인해 업스트림 flow를 중단하는데 5초가 걸립니다. 그리고, 제한 시간이 지나 업스트림 flow가 취소됩니다.

 

사용자가 앱을 다시 열 경우 업스트림 flow가 자동으로 다시 시작됩니다.

 

 

하지만 회전 시나리오에서 뷰는 잠시 중단됩니다. 따라서 StateFlow는 절대 복원되지 않고 모든 업스트림 flow를 활성상태로 유지하며 아무 일도 없었던 것처럼 사용자에게 회전 인스턴스를 보냅니다.

 StateFlow를 사용하여 ViewModel에서 flow를 노출하거나 asLiveData를 사용하여 이와 동일한 작업을 수행하는 것이 좋습니다.

 


repeatOnLifecycle 내부 구현 코드

repeatOnLifecycle은 구글에서 권장하는 UI에서 데이터를 안전하게 수집하도록 하는 API입니다.

앞선 내용에서 launchWhenX API가 작업을 시작 -> 중단 -> 재개한다는 특징이 있다면 repeatOnLifecycle은 시작 -> 중단(취소) -> 재시작한다는 특징이 있다고 했습니다.

 

내부 구현 코드를 통해 동작 원리를 확인해보겠습니다.

public suspend fun Lifecycle.repeatOnLifecycle(
    state: Lifecycle.State,
    block: suspend CoroutineScope.() -> Unit
) {
   	..
    // This scope is required to preserve context before we move to Dispatchers.Main
    coroutineScope {
        withContext(Dispatchers.Main.immediate) {
            // Check the current state of the lifecycle as the previous check is not guaranteed
            // to be done on the main thread.
            if (currentState === Lifecycle.State.DESTROYED) return@withContext

            // Instance of the running repeating coroutine
            var launchedJob: Job? = null

            // Registered observer
            var observer: LifecycleEventObserver? = null
            try {
                // Suspend the coroutine until the lifecycle is destroyed or
                // the coroutine is cancelled
                suspendCancellableCoroutine<Unit> { cont ->
                    // Lifecycle observers that executes `block` when the lifecycle reaches certain state, and
                    // cancels when it falls below that state.
                    val startWorkEvent = Lifecycle.Event.upTo(state)
                    val cancelWorkEvent = Lifecycle.Event.downFrom(state)
                    val mutex = Mutex()
                    observer = LifecycleEventObserver { _, event ->
                        if (event == startWorkEvent) {
                            // Launch the repeating work preserving the calling context
                            launchedJob = this@coroutineScope.launch {
                                // Mutex makes invocations run serially,
                                // coroutineScope ensures all child coroutines finish
                                mutex.withLock {
                                    coroutineScope {
                                        block()
                                    }
                                }
                            }
                            return@LifecycleEventObserver
                        }
                        if (event == cancelWorkEvent) {
                            launchedJob?.cancel()
                            launchedJob = null
                        }
                        if (event == Lifecycle.Event.ON_DESTROY) {
                            cont.resume(Unit)
                        }
                    }
                    this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver)
                }
            } finally {
                launchedJob?.cancel()
                observer?.let {
                    this@repeatOnLifecycle.removeObserver(it)
                }
            }
        }
    }
}

 

전체 코드는 위와 같으며, 하나하나 핵심 코드를 살펴 보도록 하겠습니다.

 

① 사용되는 주요 매개변수

public suspend fun Lifecycle.repeatOnLifecycle(
    state: Lifecycle.State,  // 블록 실행을 시작할 최소 Lifecycle 상태
    block: suspend CoroutineScope.() -> Unit // 해당 상태에서 실행할 suspend 블록
)
  • state: 새로운 코루틴이 실행될 최소한의 Lifecycle.State 값입니다.
    이 상태를 벗어나면 실행 중인 작업이 취소(cancel) 되며, 다시 상태에 도달하면 새로 시작(restart) 됩니다.
  • block: 전달된 state 이상일 때 실행할 일시 중단(suspend) 가능 블록입니다.

② 내부 동작 과정

코드 흐름을 단계별로 분석해 보면 다음과 같습니다.

1️⃣ coroutineScope 내부에서 Dispatchers.Main.immediate 컨텍스트로 이동

coroutineScope {
    withContext(Dispatchers.Main.immediate) {
  • coroutineScope를 사용하여 현재 실행 컨텍스트를 유지합니다.
  • Dispatchers.Main.immediate를 사용하여 UI 스레드에서 즉시 실행되도록 합니다.
    • Android에서는 UI 업데이트가 즉각적으로 반영되어야 하므로 Main 스레드에서 실행하는 것이 중요합니다.

2️⃣ Lifecycle.State.DESTROYED 확인 후 즉시 종료 가능성

if (currentState === Lifecycle.State.DESTROYED) return@withContext
  • 현재 Lifecycle.State가 DESTROYED라면 더 이상 작업을 수행할 필요가 없으므로 즉시 종료합니다.

3️⃣ suspendCancellableCoroutine을 통해 취소 가능한 코루틴 생성

suspendCancellableCoroutine<Unit> { cont ->
  • 현재 Lifecycle의 상태를 감지하며, 취소가 가능한 코루틴을 생성합니다.

4️⃣ LifecycleEventObserver를 생성하여 상태 변화 감지

val startWorkEvent = Lifecycle.Event.upTo(state)
val cancelWorkEvent = Lifecycle.Event.downFrom(state)
val mutex = Mutex()
observer = LifecycleEventObserver { _, event ->
  • startWorkEvent: Lifecycle.State가 지정된 state에 도달하면 실행될 이벤트
  • cancelWorkEvent: state 아래로 내려가면 실행될 이벤트

5️⃣ Lifecycle이 지정된 상태에 도달하면 새로운 코루틴 시작

if (event == startWorkEvent) {
    launchedJob = this@coroutineScope.launch {
        mutex.withLock {
            coroutineScope {
                block()
            }
        }
    }
    return@LifecycleEventObserver
}
  • 지정된 Lifecycle.State에 도달하면 새로운 코루틴을 실행하여 block()을 수행합니다.
  • mutex.withLock을 사용하여 연속적인 실행을 방지하고, coroutineScope를 통해 내부의 모든 코루틴이 완료될 때까지 실행됩니다.

6️⃣ Lifecycle이 지정된 상태 아래로 내려가면 Job 취소

if (event == cancelWorkEvent) {
    launchedJob?.cancel()
    launchedJob = null
}
  • Lifecycle.State가 지정된 상태 아래로 내려가면, 실행 중인 Job을 취소하고 null로 설정합니다.

7️⃣ Lifecycle이 ON_DESTROY되면 Observer 제거

if (event == Lifecycle.Event.ON_DESTROY) {
    cont.resume(Unit)
}
  • ON_DESTROY 이벤트가 발생하면 cont.resume(Unit)을 호출하여 코루틴을 종료하고, 더 이상 Lifecycle을 감지하지 않습니다.

8️⃣ finally 블록에서 Observer 및 Job 정리

finally {
    launchedJob?.cancel()
    observer?.let {
        this@repeatOnLifecycle.removeObserver(it)
    }
}
  • 최종적으로 실행 중인 Job을 취소하고, 등록된 Lifecycle Observer를 제거하여 메모리 누수를 방지합니다.

이러한 형태가 Lifecycle Event에 의해 반복적으로 발생되기 때문에 repeatOnLifecycle의 방식이 작업을 시작 -> 중단(취소) -> 재시작한다고 볼 수 있습니다.

 


repeatOnLifecycle 사용시 주의점

repeatOnLifecycle은 지정된 Lifecycle.State에 도달하면 블록 내의 자식 코루틴을 실행하고,

해당 상태를 벗어나면 실행 중인 모든 자식 코루틴을 취소하는 특징을 가지고 있습니다.

 

즉, repeatOnLifecycle 내부에서 send 또는 collect를 수행하면 Lifecycle 상태 변화에 따라 자동으로 취소됩니다.
하지만, 새로운 스코프(GlobalScope 또는 별도의 CoroutineScope)를 사용하면 라이프사이클과 무관하게 동작하므로, 주의가 필요합니다.

 

예를들어, 아래와 같은 예시를 들 수 있습니다.

잘못된 사용 예제 1: send, collect 모두 취소 되지 않음

 override fun onCreate(savedInstanceState: Bundle?) {
        //send
        val countingFlow: Flow<Int> = flow {
            var count = 0
            repeat(100) {
                delay(1000L)
                println("emit: ${it}")
                emit(count++)
            }
        }
        
        //receive
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                GlobalScope.launch {
                    flow.collect { value ->
                        println("collected: ${it}")
                    }
                }
            }
        }        
        
}

 

[실행 결과]

emit:1
collected:1
emit:2
collected:2
emit:3
collected:3
<----[onStop]---->
emit:4
collected:4
emit:5
collected:5
<---[onResume]-->
emit:6
collected:6
...
...
  • GlobalScope.launch 내부에서 collect를 실행했기 때문에 Lifecycle과 무관하게 계속 데이터가 수집됨.
  • Activity가 onStop() 상태가 되어도 Flow가 계속 동작하므로, 불필요한 리소스 낭비 및 메모리 누수 위험 발생

해결 방법 → repeatOnLifecycle 내부에서 collect를 직접 실행

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        countingFlow.collect { value ->
            println("collected: ${value}")
        }
    }
}

 

잘못된 사용 예제 2: collect는 중단되지만 send는 계속 동작

//viewModel

private val _sharedFlow = MutableSharedFlow<Int>()
val sharedFlow = _sharedFlow.asSharedFlow()

init{
    sendData()
}

fun sendData() {
    viewModelScope.launch {
    	repeat(100) { value ->
          println("emit: $valuue")
          _sharedFlow.emit(value) // ViewModelScope에서 안전하게 데이터 전송
        }
    }
}
// Activity or Fragment
viewmdoel.sendData()
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewmodel.sharedFlow.collect { value ->
            println("collected: $value")
        }
    }
}

 

[실행 결과]

emit:1
collected:1
emit:2
collected:2
emit:3
collected:3
<----[onStop]---->
emit:4
emit:5
<---[onResume]-->
emit:6
collected:6
...
...
  • repeatOnLifecycle 내부에서 collect를 실행했으므로 Lifecycle이 STOPPED 상태가 되면 수집(collect)은 자동으로 중단됩니다.
  • 하지만 sendData()는 viewModelScope에서 실행되므로 별도의 Job에서 실행됩니다. 따라서, Lifecycle과 관계없이 계속 데이터 방출이 진행됩니다.
  • 즉, 사용자가 화면을 보고 있지 않아도 불필요하게 데이터가 계속 생성됩니다.


LiveData와 비교

이 API가 LiveData와 비슷하게 동작한다는 사실을 눈치챘을 수도 있습니다. LiveData는 Lifecycle aware하며, 재시작 동작으로 UI에서 데이터 스트림을 관찰하는데 이상적입니다. Lifecycle.repeatOnLifecycle, Flow.flowWithLifecycle API의 경우도 마찬가지입니다.

이런 API를 사용하여 flow를 수집하는 것은 Kotlin-only 앱의 LiveData를 자연스럽게 대체하는 것입니다. 해당 API를 flow 수집에 사용할 경우 LiveData는 coroutine과 flow에 비해 어떠한 이점도 제공하지 않습니다. flow는 모든 Dispatcher에서 수집할 수 있고 모든 연산자와 함께 사용할 수 있기 때문에 더욱 유연합니다. 제한된 연산자만 사용할 수 있고 UI 스레드에서 항상 값이 관찰되는 LiveData와는 반대로 말이죠.

 


정리

Activity에서 Flow를 사용할 때 주의해야 할 점 중 하나는 lifecycleScope에서 collect를 수행할 경우, Activity가 onDestroy될 때만 데이터 수집이 중단된다는 것입니다.

하지만 앱이 백그라운드로 전환될 경우(onStop 호출) lifecycleScope에서 실행된 collect는 계속 동작하게 됩니다. 이는 불필요한 리소스 낭비로 이어질 수 있습니다.

 

이 문제를 해결하려면 Activity가 백그라운드로 전환될 때(onStop) collect를 수행하는 코루틴을 명시적으로 취소하고, 다시 포그라운드로 돌아왔을 때(onStart) 새로운 코루틴을 생성해 collect를 재개해야 합니다. 하지만 이 방식은 많은 보일러플레이트 코드를 요구합니다.

 

이를 간편하게 해결하기 위해 Android에서는 repeatOnLifecycle API를 제공합니다. 이 API는 지정된 Lifecycle.State 범위 내에서(onStart~onStop) Flow를 안전하게 collect할 수 있도록, 해당 상태에 진입할 때마다 새로운 Coroutine Job을 생성하고, onStop에서 자동으로 취소하는 구조를 제공합니다. 이를 활용하면 불필요한 리소스 낭비 없이 효율적으로 Flow를 사용할 수 있습니다.

 


참고자료

https://tourspace.tistory.com/434#google_vignette

 

[Coroutine] State flow vs Shared flow with case study

Flow를 사용하면서 유용하게 사용할 수 있는 state flow와 shared flow가 다른 점과 각각 어떤 상황에서 적합한지를 알기 위하여 두 개의 특성을 비교하려고 합니다. Flow builder로 생성한 flow들은 기본적

tourspace.tistory.com

https://kotlinworld.com/228

 

[CoroutineScope] 3. repeatOnLifecycle 사용하여 불필요한 메모리 사용 방지하기 : flow의 올바른 데이터 수

lifecycleScope의 한계점 flow를 Activity의 lifecycleScope에서 사용하게 되면 Activity가 onDestroy 될 때 데이터 collect가 중단된다. 하지만 onDestroy는 Activity가 종료될 때 수행되고 Activity가 백그라운드로 내려갈

kotlinworld.com

https://hongbeomi.medium.com/%EB%B2%88%EC%97%AD-android-ui%EC%97%90%EC%84%9C-flow%EB%A5%BC-%EC%88%98%EC%A7%91%ED%95%98%EB%8A%94-%EC%95%88%EC%A0%84%ED%95%9C-%EA%B8%B8-bd8449e67ec3

 

[번역]Android UI에서 flow를 수집하는 안전한 길

Android UI에서 flow를 안전하게 수집하기

hongbeomi.medium.com

https://medium.com/hongbeomi-dev/%EC%A0%95%EB%A6%AC-%EC%BD%94%ED%8B%80%EB%A6%B0-flow-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-android-dev-summit-2021-3606429f3c5f

 

[정리] — 코틀린 Flow 사용하기 (Android Dev Summit 2021)

Android Dev Summit 2021

medium.com

https://hanyeop.tistory.com/437

 

[Android] 코루틴 Flow 생명주기 관리하기 (launchWhenStarted, repeatOnLifecycle)

2022.03.08 - [Android/AAC, MVVM] - [Android] 코루틴 StateFlow, SharedFlow 사용하기 (vs LiveData) [Android] 코루틴 StateFlow, SharedFlow 사용하기 (vs LiveData) 2021.05.13 - [Android/AAC, MVVM] - [Android] Room + LiveData + ViewModel + DataBind

hanyeop.tistory.com