Developing Myself Everyday
article thumbnail

State란?


State는 객체 지향 관점에서 자주 사용되는 단어로 객체가 특정 시점에서 어떤 데이터 값을 가지고 있는지 나타내는 것으로, 객체의 특성이나 속성을 나타냅니다.

 

 

안드로이드의 UI 레이어 가이드에서는 UI 레이어의 UI State를 생성하고 관리하는 수단으로 단방향 데이터 흐름(UDF)을 설명합니다.

 

이러한 State는 UI와 관련된 데이터를 저장하고 관리하는데 사용되는 상태 홀더 ViewModel을 통해 관리됩니다.

 

상태 홀더 ViewModel은 앱이 상태를 읽을 수 있도록 상태를 저장합니다. 로직이 필요한 경우 필요한 로직을 호스팅하는 데이터 소스에 대한 액세스 권한을 제공합니다.

 

 

LiveData

이러한 State 데이터를 다루기 위해 안드로이드에서는 LiveData를 사용했습니다. 다만 LiveData는 액티비티의 LifeCycle에 반응해 데이터의 변경 사항을 관찰하고 이에 대응합니다.

 

그렇다면 만약 액티비티가 있는 `UI 레이어`가 아닌 `Domain 레이어`와 상호작용한다면 어떻게 될까요?? 기존의 LiveData를 사용할 수가 없어집니다.

 

이럴 때 사용할 수 있는 API가 바로 Flow입니다.

 

 

 

StateFlow와 SharedFlow란?


StateFlow SharedFlow 최적으로 State 데이터를 내보내고 여러 소비자에게 값을 내보낼 수 있는 Flow API 입니다.

 

FlowStateFlow SharedFlow는 동작 방식에 차이가 있습니다. StateFlow SharedFlow는 Hot Stream 방식이고 Flow는 Cold Stream 방식입니다.

 

 

Cold Stream ❄️ vs Hot Stream 🔥

Cold Stream은 Flow 를 수집하는 각각의 Collector 들이 데이터를 수집 할 때마다 새로운 데이터 스트림을 생성하므로 Collector 들은 각각의 개별적인 데이터 스트림에서 데이터를 수집합니다. 또한 스크림을 생성 후 

이를 소비하지 않으면 아무것도 수행하지 않습니다.

 

Hot Stream은 Flow 를 수집하는 각각의 Collector 들이 데이터 스트림을 공유하여 동일한 데이터를 수집하며 구현에 따라 Collector 유/무에 따라 시작되기도 하지만 기본적으로 Collector 가 없어도 데이터 제공자(Provider)는 데이터 스트림을 제공합니다.

 


일반적인 Flow는 상태를 가질 수 없기 때문에 데이터 홀더의 역할을 하지 못합니다.

 

 

StateFlow

StateFlowLiveData와 매우 유사합니다. 다만 State를 가질 수 있다는 차이점이 존재합니다. 

 

StateFlow 상태를 표현하기 때문에 초기값이 필요하고 하나의 값만 업데이트할 수 있습니다.

private val _stateFlow = MutableStateFlow("Hello World")
val stateFlow = _stateFlow.asStateFlow()

fun triggerStateFlow() {
    _stateFlow.value = "StateFlow"
}

 

또한 StateFlow는 가장 최신의 값을 흘려보내 줍니다. 

 

 

SharedFlow

SharedFlow는 늘 기본값을 가져야 하는 상태와 달리, 이벤트라는 형태로 값을 전달합니다.

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

fun triggerSharedFlow() {
    viewModelScope.launch {
        _sharedFlow.emit("SharedFlow")
    }
}

 

 

이러한 Flow 들은 아래와 같은 계층 구조를 가지고 있습니다.

 

Flow <- SharedFlow <- StateFlow

 

 

 

StateFlow vs SharedFlow

SnackBar를 표시하는 상황이라고 가정해 보겠습니다. 이러한 상황에서 StateFlow SharedFlow 중 어느 것을 사용해야 할지 아직은 바로 답하기 어려울 것 같기에 직접 구현해보면서 이해하고자 합니다.

 

아래와 같이 `MainActivity`에서 StateFlow 의 상태를 변화시키고 이를 collect한 다음 SnackBar를 출력해 보겠습니다.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.stateBtn.setOnClickListener { viewModel.triggerStateFlow() }
        binding.sharedBtn.setOnClickListener { viewModel.triggerSharedFlow() }

        lifecycleScope.launchWhenStarted {
            viewModel.stateFlow.collectLatest {
                Snackbar.make(
                    binding.root,
                    it,
                    Snackbar.LENGTH_LONG
                ).show()
            }
        }
    }

}

 

 

버튼을 클릭하면 아래와 같이 스낵바를 얻을 수 있습니다. 버튼을 다시 클릭한다고 해도 스낵바는 생성되지 않습니다. 상태가 변하지 않기 때문입니다.

 

 

 

그런데 화면이 회전하는 등의 행위로 액티비티가 재생성 된다면, StateFlow는 자동으로 value를 다시 emit 해주게 됩니다. 그래서 스낵바가 생성되게 됩니다.

 

 

이러한 StateFlow의 특성은 스낵바와 같은 일회성 작업을 하고 싶을 때는 적합하지 않습니다. 한번 이제  SharedFlow를 사용해 보겠습니다. 스낵바를 생성하는 부분만 아래와 같이바꿔서 진행해 보겠습니다.

lifecycleScope.launchWhenStarted {
    viewModel.sharedFlow.collectLatest {
        Snackbar.make(
            binding.root,
            it,
            Snackbar.LENGTH_LONG
        ).show()
    }
}

 

 

SharedFlow는 상태가 아닌 이벤트를 전달하기에 버튼을 몇번이나 누르던 스낵바를 생성합니다.

 

 

그리고 액티비티가 재생성 될때에는 스낵바를 생성하지 않습니다. 이는 SharedFlow가 One-Time 이벤트에 매우 적합하다는 것을 의미합니다.

 

 

 

마치며


이렇게 해서 상태와 StateFlow, SharedFlow에 대해 알아보았습니다. 각각의 사용법이 다르기에 앞으로 프로젝트를 진행하면서 원하는 방식을 채용할 수 있겠다는 생각이 듭니다.

 

 

Reference

 

상태 홀더 및 UI 상태  |  Android 개발자  |  Android Developers

상태 홀더 및 UI 상태 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. UI 레이어 가이드에서는 UI 레이어의 UI 상태를 생성하고 관리하는 수단으로 단방향 데이

developer.android.com

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!