안드로이드/Android 비동기 처리

[Android] Coroutine StateFlow, Flow, LiveData, ViewModelScope, LifecycleScope + Paging3

코딩하는후운 2024. 3. 20. 13:26
반응형

Coroutine StateFlow, Flow, LiveData, ViewModelScope, LifecycleScope + Paging3

 

StateFlow SharedFlow는 흐름에서 최적으로 상태 업데이트를 내보내고 여러 소비자에게 값을 내보낼 수 있는 Flow API입니다.

# StateFlow

현재 상태와 새로운 상태 업데이트를 수집기에 내보내는 관찰 가능한 상태 홀더 흐름입니다.

value속성을 통해서도 현재 상태 값을 읽을 수 있습니다.

상태를 업데이트하고 흐름에 전송하려면 MutableStateFlow 클래스의 value 속성에 새 값을 할당합니다.

 

private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
val uiState: StateFlow<LatestNewsUiState> = _uiState

init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                    .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
      }
}
  • MutableStateFlow : 업데이트를 담당하는 클래스가 생산자
  • StateFlow에서 수집되는 모든 클래스가 소비자입니다.

StateFlow는 항상 활성 상태이고 메모리 내에 있으며 가비지 컬렉션 루트에서 달리 참조가 없는 경우에만 가비지 컬렉션에 사용할 수 있습니다.

 

View는 다른 흐름과 마찬가지로 StateFlow를 수신 대기합니다.

lifecycleScope.launch {            
            repeatOnLifecycle(Lifecycle.State.STARTED) {               
                latestNewsViewModel.uiState.collect { uiState ->                 
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }

# 경고

UI를 업데이트해야 하는 경우 launch 또는 launchIn 확장 함수로 UI에서 직접 흐름을 수집하면 안됩니다.이러한 함수는 뷰가 표시되지 않는 경우에도 이벤트를 처리합니다.이 동작으로 인해 앱이 다운될 수 있습니다.방지하려면 repeatOnLifecycle API를 사용합니다.

 

# StateFlow, Flow, LiveData

StateFlow와 LiveData는 비슷한 점이 있다.

  • 둘다 관찰 가능한 데이터 홀더 클래스
  • 앱 아키텍처에 사용할 때 비슷한 패턴을 따른다

그러나 StateFlow와 LiveData는 다은과 같이 다르게 작동합니다.

  • StateFlow의 경우 초기 상태를 생성자에 전달해야 하지만 LiveData의 경우는 전달하지 않습니다.
  • 뷰가 STOPPED 상태가 되면 LiveData.observe()는 소비자를 자동으로 등록취소,
    StateFlow 또는 다른 흐름에서 수집하는 경우 자동으로 수집을 중지하지 않습니다.
    동일한 동작을 실행하려면 Lifecycle.repeatOnLifecycle블록에서 흐름을 수집해야 합니다.

 

# Flow와 StateFlow의 차이

Flow는 데이터 흐름이다.

데이터 흐름(flow)을 발생시키기만 할 뿐 데이터가 저장되지 않는다.

따라서 flow만을 이용해 안드로이드의 UIState를 업데이트 하기 위해서는 두가지 방법이 가능했다.

  1. 화면이 재구성 될때마다 서버 혹은 DB로부터 데이터 가져오기
  2. Flow로부터 collect한 데이터를 ViewModel에 저장해놓고 사용

1번은 비효율적이다.

2번은 효율적이다.

: ViewModel이 onDestroy가 호출되더라도 살아있고 ViewModel에서 해당 데이터를 저장하고 있으면 되기 때문이다.

 

하지만, 2번 방법에서 데이터를 저장하고 있으려면 별도의 데이터 홀더 변수를 만들어야 한다.

또한 데이터 홀더 변수는 Reactive하지 않기 때문에 UI에서 해당 데이터 홀더 변수를 구독하기 위해서는 별도의 fetching 로직을 만들어야 한다.

*fetching로직: 데이터를 가져오는 로직

 

2번을 구현하는 다른방법은 ViewModel에서 데이터 홀더 변수와 flow를 같이 사용하는 것.

flow를 구독하고 데이터 홀더 변수는 flow에서 마지막으로 발행한 데이터를 저장하고 있으면 된다.

UI에서는 flow에서 값을 발행하기 전에는 데이터 홀더 변수의 데이터를 사용하면 된다.

 

문제는 이 둘 모두 보일러 플레이트 코드를 만들어 낸다는것.

모두를 구독하기 위해서 비슷한 코드를 매번 작성해 가독성을 떨어트리는 것을 지양해야 한다.

 

이러한 문제점을 해결하기 위해 등장한 것이 바로 StateFlow이다.

 

 

# StateFlow의 등장과 Flow의 한계 극복

StateFlow는 데이터 홀더(저장소) 역할을 하면서 Flow의 데이터 스트림 역할까지 한다.

UI단에서 StateFlow를 구독해 UIState(UI를 위한 데이터)를 업데이트 하면 화면이 재구성될 때 마다 다시 서버로 데이터를 요청할 필요가 없어진다.

 

 

예를들어, 영화 평점 앱을 만든다고 할 경우 영화에 대한 정보, 사용자에 대한 정보, 영화의 평점에 대한 정보를 각 테이블에서 가져와 하나의 객체로 만들어야 한다.

 

하나로 만들어진 Flow는 UI에서 사용되기 위해 StateFlow로 변환되어야 한다.

이 UI에서는 이 StateFlow를 구독하여 항상 최신 데이터를 발행 받는다.

 

이것이 가능하기 위해서는 Flow를 StateFlow로 변환하는 로직이 필요하다.

또한 StateFlow가 항상 Flow를 구독하고 있으면 메모리 누수가 생기므로 이 StateFlow가 살아있어야 하는 CoroutineScope를 명시할 수 있어야 한다.

우리는 이를 stateIn 함수를 통해 할 수 있다.

 

 

# stateIn 사용하여 Flow를 StateFlow로 변환하기(콜드 흐름 -> 핫 흐름)

Cold Stream

  • 하나의 소비자(Consumer)에게 값을 보냅니다.
  • 생성된 이후에 누군가 소비하기 시작하면 데이터를 발행합니다.

예를 들어 상태가 변하지 않는 값을 읽을 때

데이터베이스를 읽거나 URL을 통해서 서버 값을 읽는 경우 Cold Stream으로 구현할 수 있습니다.

Hot Stream

  • 하나 이상의 소비자(Consumer)에게 값을 보낼 수 있습니다.
  • 데이터 발행이 시작된 이후 부터 모든 소비자에게 같은 데이터를 발행하고 구독자가 없는 경우에도 데이터를 발행합니다.

예를 들어 상태가 변하는 값을 읽을 때 네트워크 상태 정보 값을 얻어올 때 Hot Stream을 사용할 수 있습니다.

 

 

 

StateIn함수를 사용하면 Flow를 StateFlow로 변환할 수 있다.

stateIn은 세가지 변수를 받는다.

  • scope : StateFlow가 Flow로부터 데이터를 구독받을 CoroutineScope를 명시한다.
  • started : Flow로부터 언제부터 구독을 할지 명시할 수 있다.
  • initialVlue : StateFlow에 저장될 초기값을 설정한다.

예제

val stateFlow = stringFlow.stateIn(
    initialValue = "integer 0",
    started = SharingStarted.WhileSubscribed(5000),
    scope = viewModelScope
)
  • 이렇게 하면 초기 저장값은 "integer 0"이고
  • WhileSubscribed는 collector가 없어졌을 때 지정된 시간 이후 StateFlow가 동작을 멈추도록 만드는 값
  • ViewModel의 생명주기만큼만 구독받는 행동을 하는 StateFlow

 

# ViewModelScope

ViewModel을 대상으로 정의됩니다.

이 범위에서 시작된 모든 코루틴은 ViewModel이 삭제되면 자동으로 취소됩니다.

 

viewModelScope는 ViewModel에서 onCleared() 호출 할때 직접 coroutine context를 명시적으로 취소를 하지않아도 자동적으로 onCleared() 호출 될때 coroutine 작업을 취소합니다.

 

ViewModelScope를 사용하지 않고 ViewModel에서 Coroutine을 사용한다면 onCleared()에서 직접 job.cancel()을 통해서 Coroutine작업을 취소했었어야 했습니다.

그래야 메모리 누수를 방지할 수 있었다.

 

구글 Android팀에서 ViewModel의 onCleard가 호출 된다면 자동적으로 ViewModelScope의 작업을 취소하도록 만들어 놨다.

 

# ViewModelScope 내부 원리

  • ViewModelScope는 Kotlin으로 작성되어있다.
  • ViewModel의 확장 프로퍼티로 사용이 가능하다.
  • getter로 CoroutineScope를 리턴해준다.
  • getter에서 CloseableCoroutineScope() 클래스를 생성할 때 매개변수로 SupervisorJob() + Dispatchers.Main.immediate를 넘겨줍니다.
    ViewModelScope는 기본적으로 Main thread로 작업을 하는 것임을 알수가 있습니다.

return하는 setTagIfAbsent()함수는 아래와 같다.

ViewModel의 mBagOfTags라는 HashMap에 중복되는 key가 없다면 인자로 받아온 key, newValue를 mBagOfTag에 put하고 난 후 매개변수로 받은 newValue를 return

 

onCleared() 함수는 ViewModel의 clear()라는 함수로 인해 호출이 된다.

clear()함수가 호출 되면 mBagOfTags에 있는 모든 value를 close()합니다.

그 다음 onCleared()를 호출합니다.

Closeable 인터페이스를 상속 받아 close함수에 coroutinecontext객체가 cancel()되는 것을 알 수 있다.

 

clear메서드는 오직 AndroidX내부에서 호출되기 위해서 만들어졌으며 프레임워크에서 사용되는 함수이다.

Activity 혹은 Fragment의 LifeCycle이 DESTROY가 호출될 때 사용됩니다.

 

# 정리

  1.  ViewModelScope안에서 mBagOfTags에 CloseableCoroutineScope를 통해서 해당 CoroutineContext를 put
  2.  Lifecycle == DESTROY 일때 clear 함수가 호출됨.
  3.  clear 함수에서 mBagOfTags의 값들을 모두 close() 처리
  4. CloseableCoroutineScope class에서 정의한 close()의 내용이 coroutineContext.cancel() 임으로, 해당 작업을 취소함.

위와 같은 과정으로 LifeCycle DESTORY 일때 알아서 CoroutineContext가 취소 되기 때문에 viewModelScope를 사용하면 안드로이드 개발자가 뷰모델에서 편하게 Coroutine을 사용할 수 있습니다.

 

# ViewModelScope와 Retrofit2 사용시

* ViewModelScope의 Dispatchers를 바꾸지 않아도 되는 이유

위 설명대로라면 ViewModelScope는 Main Thread로 작업을 합니다.

그런데 Retrofit2를 사용할 때 IO Thread가 아니더라도 문제없이 Retrofit2를 사용할 수 있습니다.

 

이유?

우선 Retrofit2를 사용할 때 Coroutine을 응용해서 사용한다면 보통 아래와 같이 사용이 된다.

기본적으로 Call<Product>를 사용했던 것과 달리 CoroutineScope에서 실행되는 suspend 함수로 지정하면 Retrofit Package를 이용하는 개발자는 return type에 Retrofit2에서 제공하는 Call<T>를 사용합니다.

(Retrofit를 만든 square에서 Coroutine 사용을 돕기 위한 kotlinExtensions를 지원해주기 때문입니다.)

위 코드를 보면 애초에 enqueue를 통해서 내부적으로 통신을 통한 결과값을 받아서 통신이 성공/실패 여부를 알려주고 있었습니다. 

즉, 내부적으로 이미 통신을 해서 responseBody를 알려주고 enqueue로 결과 값을 활용하고 있었습니다. 

 

 

# LifecycleScope

Lifecycle 객체에서 정의.

이 범위에서 실행된 코루틴은 Lifecycle이 끝날 때 제거됩니다.

 

# LifecycleScope의 한계점

flow를 Activity의 lifecycleScope에서 사용하게 되면 Activity가 onDestroy될 때 데이터 collect가 중단된다.

앱이 백그라운드로 내려가면 onStop시 수행되므로 lifecycleScope의 collect는 계속해서 데이터를 수집한다.

 

기존의 해결책

기존에는 이 문제를 해결하기 위해 onStart에서 collect를 시작하고 onStop에서 collect를 중지하는 작업을 했다.

하지만 보일러플레이트 코드를 만들어 냈으며, 사람이 실수할 경우 불필요하게 데이터 수집이 백그라운드에서 일어날 수 있었다.

 

코루틴의 새로운 버전에서는 위의 문제를 처리하기 위한 repeatOnLifecycle이라는 메서드가 생겼다.

Coroutine Job이 생성될 Lifecycle.State을 인자로 받아 특정 생명주기가 포그라운드에 있을때까지만(onStart ~ onStop) 동작하는 Coroutine Job을 생성해주는 메서드이다.

 

 

 

참조 :

https://kotlinworld.com/232

https://kotlinworld.com/233?category=973477

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ko 

https://3edc.tistory.com/69

https://kotlinworld.com/228

https://kotlinworld.com/198

https://developer.android.com/topic/libraries/architecture/coroutines?hl=ko 

https://gift123.tistory.com/60

 

반응형