StateFlow는 코루틴 플로우의 하위 클래스로, 상태를 나타내는 값을 지속적으로 배출할 수 있습니다. StateFlow는 매우 유사한 동작을 수행하는 LiveData와 비교하여 몇 가지 장점이 있습니다.
먼저 StateFlow는 Kotlin 코루틴에서 제공하는 코루틴 범위(Coroutine Scope)에서 안전하게 사용됩니다. 이는 Activity/Fragment가 파괴되거나 View가 분리된 경우 데이터 누출을 방지할 수 있다는 것을 의미합니다.
또한 StateFlow는 불변성(immutable)을 보장합니다. LiveData와 달리 StateFlow에서는 값을 변경할 수 없으며, 새로운 상태 값을 배출할 때마다 새로운 객체가 생성됩니다. 이를 통해 예기치 않은 값 변경을 방지하고, 디버깅과 테스트를 용이하게 만들어줍니다.
간단하게 StateFlow를 이용해 숫자의 증가와 감소를 출력해주는 UI를 만들어보겠습니다.
class MainViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increment() {
_count.value++
}
fun decrement() {
_count.value--
}
}
위 코드에서는 MutableStateFlow 클래스를 사용하여 _count라는 변수를 선언하고, StateFlow 타입의 count 변수에 할당합니다. 그리고 increment()와 decrement() 메소드를 사용하여 _count 값을 증가시키거나 감소시킵니다.
이제 액티비티에서 StateFlow의 값을 사용해보겠습니다.
class MainActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
lifecycleScope.launch {
viewModel.count.collect { value ->
countTextView.text = value.toString()
}
}
incrementButton.setOnClickListener {
viewModel.increment()
}
decrementButton.setOnClickListener {
viewModel.decrement()
}
}
}
위 코드에서는 lifecycleScope를 사용하여 ViewModel의 count 값을 관찰하고, UI 업데이트를 수행합니다. increment()와 decrement() 메소드를 사용하여 ViewModel의 상태 값을 변경할 수 있습니다.
StateFlow를 HotFlow로 변환하여 즉시 값을 내보내도록 만들 수 있습니다. 그 방법으로는 2가지가 있습니다.
conflate()
shareIn()
위의 두 함수는 모두 Cold Flow를 Hot Flow로 변환해주는 함수입니다. 하지만 두 함수는 각각 다른 방식으로 Flow를 처리합니다. 각 함수의 차이점과 각각의 장단점에 대해서 작성해보겠습니다.
conflate() 함수는 Cold Flow에서 나오는 이벤트를 버퍼에 담지 않고, 최신 이벤트 하나만 처리합니다. 이전 이벤트는 무시됩니다. 즉, 버퍼링 없이 최신 이벤트만 처리되는 것입니다. 이 방식은 일반적으로 UI에서 Flow를 사용할 때 유용합니다. UI에서는 이전 데이터가 중요하지 않고, 최신 데이터만 갱신되면 되기 때문입니다.
장점
단점
shareIn() 함수는 Cold Flow를 Hot Flow로 변환하는 또 다른 방법입니다. 이 함수는 Flow를 공유하면서 최신값을 유지할 수 있습니다. Hot Flow가 생성된 후에 새로운 구독자가 등록되면, 최신값을 받게 됩니다. 이 방식은 주로 데이터를 캐싱하는 데 사용됩니다.
장점
단점
StateFlow를 conflate()를 사용해 HotFlow로 변환해보는 것을 코드로 작성해보겠습니다.
class MainViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow().conflate()
fun increment() {
_count.value++
}
fun decrement() {
_count.value--
}
}
위 코드에서는 asStateFlow() 함수를 사용하여 StateFlow를 Flow로 변환하고, conflate() 함수를 사용하여 최신값만 유지하도록 합니다. 이제 ViewModel에서 값을 변경할 때마다, 최신값이 유지되면서 Flow가 즉시 값을 내보내게 됩니다.
반면 shareIn()을 사용하면 다음과 같습니다.
class MainViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
.shareIn(viewModelScope, SharingStarted.Eagerly, 1)
fun increment() {
_count.value++
}
fun decrement() {
_count.value--
}
}
위 코드에서는 shareIn() 함수를 사용하여 StateFlow를 공유하면서 HotFlow로 변환합니다. shareIn() 함수의 첫 번째 인자는 Flow를 공유할 CoroutineScope를 지정합니다. 여기서는 viewModelScope를 사용하여 ViewModel의 생명주기에 맞게 Flow를 관리합니다.
두 번째 인자인 SharingStarted는 Flow가 시작될 때 어떻게 시작될지를 지정합니다. 여기서는 SharingStarted.Eagerly를 사용하여 Flow가 생성될 때 즉시 시작되도록 지정합니다.
SharingStarted 종류와 각 특징을 설명하겠습니다
SharingStarted는 shareIn() 함수를 사용하여 Flow를 공유할 때 시작되는 이벤트입니다. SharingStarted는 다양한 종류가 있으며, 각 종류는 Flow를 언제 시작할지 결정하는 역할을 합니다.
WhileSubscribed: WhileSubscribed는 구독자가 있는 동안에만 Flow를 시작합니다. 따라서 최소한 한 명 이상의 구독자가 있어야 합니다.
Eagerly: Eagerly는 shareIn() 함수가 호출되면 Flow가 즉시 시작됩니다. 즉, 구독자가 없더라도 Flow가 시작됩니다.
Lazy: Lazy는 구독자가 생길 때까지 Flow를 시작하지 않습니다. 즉, 구독자가 있을 때만 Flow가 시작됩니다.
위의 3가지를 가장 많이 사용하며 다음과 같은 것들 또한 있습니다.
WhileSubscribed(timeoutMillis: Long): WhileSubscribed와 비슷하지만, 구독자가 없을 때 일정 시간 후에 Flow를 중단합니다. 이를 통해 불필요한 Flow 처리를 방지할 수 있습니다.
WhileSubscribedOrWait(timeoutMillis: Long): WhileSubscribedOrWait는 WhileSubscribed와 비슷하지만, 구독자가 없으면 일정 시간까지 기다린 다음 Flow를 중단합니다.
세 번째 인자인 replay는 최신값을 몇 개까지 보관할지를 지정합니다. 여기서는 1을 사용하여 최신값만 유지하도록 지정합니다.
이제 StateFlow가 HotFlow로 변환되었으므로, ViewModel에서 값을 변경할 때마다, 최신값이 유지되면서 즉시 값을 내보내게 됩니다.
StateFlow 사용시 주의사항 : StateFlow의 경우 같은 값이 들어온다면 즉 값의 변화가 없다면 emit()을 하지 않으니 주의하시기 바랍니다 !!!!
이번 글에서는 StateFlow에 대해 작성하였습니다. StateFlow은 실개발에서 많이 사용되는 기술로 숙지하면 도움이 될거라고 생각이 듭니다. 긴 글 읽어주셔서 감사합니다.