Flow와 StateFlow의 차이

thsamajiki·2022년 11월 9일
0

Coroutine

목록 보기
4/8

Flow의 한계

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

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

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

1번 방법은 비효율적이다. 예를 들어 안드로이드에서는 화면이 회전되었을 때마다 onDestroy가 호출된 후 다시 onCreate이 호출되는데 이때마다 새로운 데이터를 서버 혹은 DB로부터 가져와야 하기 때문이다.

2번 방법은 효율적이다. 아래 그림의 ViewModel이 살아있는 범위에서 볼 수 있듯이 ViewModel은 onDestroy가 호출되더라도 살아있고 ViewModel에서 해당 데이터를 저장하고 있으면 되기 때문이다.

하지만 2번 방법에서 데이터를 저장하고 있으려면 별도의 데이터 홀더 변수를 만들어야 한다. 또한 데이터 홀더 변수는 Reactive하지 않기 때문에 UI에서 해당 데이터 홀더 변수를 구독하기 위해서는 별도의 fetching 로직을 만들어야 한다.


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


2번을 구현하는 다른 방법은 ViewModel에서 데이터 홀더 변수와 flow를 같이 사용하는 것이다 flow를 구독하고 데이터 홀더 변수는 flow에서 마지막으로 발행한 데이터를 저장하고 있으면 된다 따라서 UI에서는 flow에서 값을 발행하기 전에는 데이터 홀더 변수의 데이터를 사용하면 된다. 이 방식으로 만들면 데이터 홀더 변수가 마지막 데이터를 저장하고 있으므로 다시 서버로 데이터를 요청할 필요가 없어진다. 이 방법이 가장 적합하다.

하지만 문제는 이 둘 모두 보일러 플레이트 코드를 만들어낸다는 것이다 안드로이드에서 수집하는 UIStateUI를 위한 데이터가 한 두개가 아닌데 모두를 구독하기 위해서 비슷한 코드를 매번 작성해 가독성을 떨어트리는 것은 지양해야 한다.


보일러 플레이트 : 비슷한 코드가 반복되는 것


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

StateFlow의 등장과 Flow의 한계 극복

StateFlow는 데이터 홀더 저장소 역할을 하면서 Flow의 데이터 스트림 역할까지 한다. UI단에서 StateFlow를 구독해 UIState(UI를 위한 데이터)를 업데이트 하면 화면이 재구성될 때 마다 다시 서버로 데이터를 요청할 필요가 없어진다 UI는 단순히 StateFlow를 구독만 하고 있으면 되는 것이다.
이를 표시한 것이 바로 아래의 그림이다.

하나로 만들어진 Flow는 UI에서 사용되기 위해 StateFlow로 변환되어야 한다. 이 UI에서는 이 StateFlow를 구독하여 항상 최신 데이터를 발행받는다.

이것이 가능하기 위해서는 Flow를 StateFlow로 변환하는 로직이 필요하다. 또한 StateFlow가 항상 Flow를 구독하고 있으면 메모리 누수가 생기므로 이 StateFlow가 살아있어야 하는 CoroutineScope을 명시할 수 있어야 한다. 우리는 이를 stateIn 함수를 통해 할 수 있다.


StateFlow는 Cold Stream 이 아니라 Hot Stream이다. 마지막 홀딩하고 있는 데이터를 구독하는 구독자에게 전달할 뿐 구독자가 구독할 때 발행을 위한 로직을 Trigger 하지는 않는다.


stateIn 사용하여 Flow를 StateFlow로 변환하기

stateIn 함수를 사용하면 Flow를 StateFlow로 변환할 수 있다. stateIn내부는 다음과 같다.

public fun <T> Flow<T>.stateIn(
	scope: CoroutineScope,
  	started: SharingStarted,
  	initialValue: T
): StateFlow<T> {
	val config = configureSharing(1)
	val state = MutableStateFlow(initialValue)
	val job = scope.launchSharing(config.context, config.upstream, state, started, initialValue)
	return ReadonlyStateFlow(state, job)
}

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

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

이 3가지 변수를 이용한 예제를 하나를 만들어보자.
1초마다 String 값을 발행하는 간단한 Flow를 다음과 같이 선언한다.

val stringFlow: Flow<String> = flow {
	for (i in 0 .1000) {
		emit("integer: $i")
		delay(1000)
	}
}

이 flow를 stateIn함수를 이용하여 StateFlow로 변환한다.
변수의 시작 시 저장 값은 integer 0이여야 한다. 따라서 initialValue를 0으로 한다.
started의 WhileSubscribed는 collector 가 없어졌을 때 지정된 시간 이후 StateFlow 발행을 멈추도록 만드는 값이다. 따라서 collector가 없어진 후 5초 후에 동작을 멈추도록 만들기 위해 SharingStarted.WhileSubscribed5000을 설정한다.
이 StateFlow가 Flow로부터 구독을 하는 범위는 ViewModel이 살아있을 때까지만이다. 따라서 viewModelScope을 scope 구독 범위로 넘긴다.

val stateFlow = stringFlow.stateIn(
	initialValue = "integer 0",
	started = SharingStarted.WhileSubscribed(5000),
	scope = viewModelScope
)

이렇게 하면 초기 저장값은 integer 0이고 구독 후 5초 후에 처음 발행 받고 ViewModel의 생명주기만큼만 구독받는 행동을 하는 StateFlow가 만들어진다.

profile
안드로이드 개발자

0개의 댓글