본문 바로가기
Development/Android

Android - 상태관리, LiveData 대신 StateFlow를 사용하자.

by du.it.ddu 2023. 8. 3.
반응형

현재 안드로이드 생태계는?

현재 안드로이드 생태계는 MVVM 아키텍처, 클린아키텍처가 대세가 되고, MVI 아키텍처가 점점 핫해지고 있다.
그만큼 개발 기술이 빠르게 발전하고 있고, 더욱 강력한 도구들이 생겨나고 있다는 것이다.

Google에서 Android Architecture Compoenent로 여러가지 도구를 제시하고 있고, 그 중에 ViewModel과 LiveData는 정말 핫하고 유용하며, 현재 안드로이드 앱 개발에 있어서 가장 기본적인 도구로 몇년동안 자리잡고 있다.

그러나 시간이 지나면서 이들도 구시대의 기술이 되어가는 것은 당연하다. 영원히 최고의 도구는 없으니까.
오늘은 그 중 하나인 LiveData에 대해, 그리고 이를 대체할 StateFlow에 대해서 얘기해본다.


상태관리, 그리고 아키텍처의 도입

앱 개발을 하면 상태관리에 대해 끊임없이 고민하고 고민하게 된다.
개인적인 생각이지만, "앱 = 유저로 인해 변화한 상태를 화면에 나타나는 소프트웨어" 로 생각한다.
그만큼 개발에 있어서 상태가 핵심이며, 그렇기 때문에 상태관리가 매우 중요하다고 생각한다.

우리가 MVVM, 클린아키텍처를 도입하는 이유가 무엇인가?

점점 복잡해지고 많아지는 앱의 기능, 복잡하고 다양한 데이터, 이를 위한 수 많은 코드들.
이들의 관심사와 의존성을 분리하고, 많은 계층을 나누고 파일을 나누는 이유를 생각해보자.
"버그를 발생할 가능성을 낮춰서", "다른 기능을 구현할때 영향범위가 적어서", "테스트하기 쉬워서" 등을 이유로 들 수 있다.

나는 좀 더 근본적인 이유로, 복잡한 로직을 단순하게 만들고 복잡한 앱의 데이터를 단순하게 하여 상태관리를 쉽게 하는 것으로 본다.

즉, 나에게 아키텍처는 결국 상태관리를 위한 고민의 결과물이다.


왜 StateFlow를 사용해야 하는가?

우리가 LiveData를 사용하는 이유는 무엇일까?

LiveData는 LifecycleOwner의 상태에 따라 옵저버의 등록과 해제, 값 콜백 등에 대한 처리가 자동으로 이루어진다.
더이상 Activity/Fragment의 Lifecycle에 따라 무언가를 섬세하게 신경쓰거나 하지 않아도 된다.

그리고 Room, Retrofit과 같은 것들과도 잘 어울린다.

여기까지만 봤을 때, LiveData는 더할나위 없이 완벽해보인다.

그러나 클린아키텍처의 관점에서 LiveData는 오로지 Presentation Layer에 해당하는 Activity/Fragment, ViewModel에서만 유효하다.

클린아키텍처의 Domain Layer는 플랫폼과 무관한 계층이다. 안드로이드를 예를 들면, 오로지 순수한 코틀린으로만 작성이 가능해야 한다. 그러나 LiveData는 안드로이드 플랫폼에 종속적인 도구다. 따라서 Domain Layer에서 사용할 수 없다.

다음으로, 클린아키텍처의 Data Layer에서 LiveData는 권장되지 않는다. LiveData는 오로지 "UI의 상태" 에 집중하여 만들어진 도구로, 값의 변화에 대한 관찰은 항상 메인 쓰레드인 한계가 있다.
즉, Data Layer의 Repository 등에서 다른 계층에서 비동기로 가져오는 LiveData 값을 관찰하지 않는 것이 좋으며, 이것은 구글에서도 권장되고 있는 사항이다. 관련해서 여기를 참고하길 바란다.

이런 관점에서, StateFlow는 순수한 코틀린 언어로 작성된 도구이므로, 모든 계층에서 무관하게 사용할 수 있는 장점이 있다.
그렇다면 StateFlow는 어떻게 사용될 수 있을까?


StateFlow를 사용할 수 있는 이유에 대하여.

StateFlow를 사용하기 전, LiveData를 대체하기 위해 필요한 몇 가지 조건을 알아보자.

첫번째, LiveData처럼 LifecycleOwner의 수명주기에 맞는 동작이 필요하다.

두번째, LifecycleOwner의 상태가 종료되기 전 까지 UI의 상태를 전달하는 스트림이 항상 유지되어야 하며, 핫 스트림이어야 한다.

세번째, 현재의 상태를 알 수 있어야 한다.

StateFlow는 위 조건을 부합할 수 있을 것인가? (부합하지 않았다면 이 글을 쓰지 않았겠지.)

기존의 Flow는 콜드 스트림이며, 현재의 값을 알 수 없기 때문에 조건에 부합하지 않았다.
그러나 SharedFlow와 StateFlow가 나오게 되었고, StateFlow는 이 조건들에 부합한다.

StateFlow에 대해선 구글 공식 문서를 보는것도 권장한다.

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

 

StateFlow 및 SharedFlow  |  Kotlin  |  Android Developers

StateFlow 및 SharedFlow 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. StateFlow와 SharedFlow는 흐름에서 최적으로 상태 업데이트를 내보내고 여러 소비자에게 값을

developer.android.com

다음은 StateFlow의 인터페이스이다.

public interface StateFlow<out T> : SharedFlow<T> {
    /**
     * The current value of this state flow.
     */
    public val value: T
}

StateFlow는 현재의 값을 알 수 있다. 따라서 세번째 조건에 부합한다.
또한, StateFlow는 Hot Stream이다. 따라서 두번째 조건에 부합한다.

첫번째 조건은 LifecycleScope와 "androidx.lifecycle:lifecycle-runtime-ktx" 에서 제공하는 repeatOnLifecycle API를 통해 구현해낼 수 있다.

따라서, 세 조건을 모두 만족한다.


StateFlow의 사용

장황한 내용들을 통해 StateFlow를 사용해야 하는 이유, 사용할 수 있는 이유에 대해 알아보았다.

그럼 이제 정말 StateFlow를 사용해보자.

MVVM 아키텍처라고 생각해보자. 그럼 ViewModel에 StateFlow를 통해 상태괸리를 하고, Activity/Fragment에서 StateFlow를 구독하여 UI를 업데이트 할 것이다.

ViewModel

class MyViewModel : ViewModel() {
    private val _myState = MutableStateFlow<MyState>(MyState.Init)
    val myState: StateFlow<MyState> = _myState.asStateFlow() 


    fun updateState() {
        viewModelScope.launch(Dispatchers.Main) {
            val newState = // something
            _myState.value = newState
            // _myState.emit(newState)
        }
    }
}

LiveData와 동일하게 값을 변화시킬 수 있는 StateFlow는 MutableStateFlow이다. 초기값을 가진 채로 생성된다.

MutableLiveData는 초기값을 주지 않아도 됐었는데, 참 아이러니하게도 Kotlin에서 Nullable로 정의하지 않아도 초기값을 주지 않으면 값이 null이었다. StateFlow를 사용하면 이러한 아이러니함도 해결할 수 있다.

Activity/Fragment

class MyAcivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModel()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch { 
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.myState.collect {
                    // update ui
                }
            }
        }
    }
}

class MyFragment : Fragment() {
    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.myState.collect {
                    // update ui
                }
            }
        }
    }
}

Activity/Fragment에서의 사용은 동일하다.

Activity라면 this(LifecycleOwner)를, Fragment라면 ViewLifecycleOwner를 사용하여 StateFlow의 데이터를 수집하는 코루틴을 실행하면 된다.

각각의 LifecycleOwner의 LifecycleScope를 사용하여 수명주기에 맞게 코루틴이 재개/중단되며, repeatOnLifecycle 함수를 통해 수명주기마다 코루틴 작업을 반복할 수 있다.

장황한 글에 비해 사용법이 상당히 간단한 것을 알 수 있다.

또한 Flow는 여러가지 강력한 API가 제공되기 때문에 사용해야할 가치과 매력이 철철 넘친다.
이제 지겨운 LiveData를 떠나 StateFlow로 새로운 상태관리 세계로 가보는 것은 어떨까?

반응형