올리브영 테크블로그 포스팅 Android Kotlin StateFlow 도입기
Android

Android Kotlin StateFlow 도입기

Kotlin Flow 누가 이제야 적용함? 안녕하세요, 올리브영입니다.

2022.12.14

안녕하세요~! 오랜만에 인사드리는 의지수입니다:) 오늘은 Kotlin Flow에 대해 소개하고 프로젝트에 도입하면서 느꼈던 작고 소중한 제 경험을 공유하고자합니다.

저도 써보고싶어요 F.L.O.W

앱 개발파트에서는 자체적으로 스터디를 하고 있습니다. 각자 주제를 정해서 공부하거나 새로 맡게되는 업무의 필요한 기술 스택들을 소개하거나..등등 새롭게 알게된 내용을 짧게나마 공유하는 시간을 갖고있어요. 시간이되면 (언제되는데 시간..) 샘플앱을 만들기도 하고요! 저는 그 스터디를 통해 해보지 못했던 클린아키텍처 기반의 구조와 StateFlow를 적용하여 샘플앱을 만들고 공유하게 되었습니다.

그런데 때마침..!!! 제가 하단 네비게이션바 드로워 메뉴에 있는 설정 화면 리팩토링을 진행하게되었어요. 리팩토링을 진행하면서 새로운 기술 스택을 도입할 수 있게되었고 당당히(냅다) 얘기했습니다. 저희 앱에도 써보고싶어요… 그리하여 적용하게되었습니다. StateFlow

LiveData 쓰면 안되나요?

안되나요.jpeg

LiveData란 Observable한 데이터 홀더 클래스입니다.

LiveData는 Activity, Fragment 등 안드로이드 컴포넌트의 생명주기를 인식합니다.

생명주기를 인식하기 때문에 생명주기가 끝나는 즉시 관찰을 멈추고 삭제되어 메모리 누수를 걱정하지 않아도 됩니다.

데이터 변화를 관찰하고 Observer 객체에 알려 UI와 데이터 상태의 일치를 보장할 수 있습니다.

LiveData는 MVVM 패턴이 아니더라도 데이터를 관리하는데 많은 사람들이 사용하고 있고 얼핏 보기에 흠잡을 것 없어보이지만, 아키텍처 관점에서 보면 아래와 같은 단점이 있습니다.

LiveData는 Android 플랫폼에 종속적이고 UI가 없는 곳에서 LiveData를 사용하기가 어렵다.

이러한 점에서 생각해봤을 때, 클린아키텍처의 Presentation Layer에서는 LiveData가 잘 동작하지만 안드로이드 플랫폼에 독립적이고, 순수 Kotlin 및 Java만 사용할 수 있는 즉 언어 의존성만 지니는 Domain Layer에서는 LiveData를 쓰기 어렵다는 것입니다. 또한 계층별로 모듈화를 진행하고 있거나 진행할 예정이라면 LiveData만을 위해 안드로이드 의존성을 지니게될 수도 있습니다.

그치만 Kotlin, Coroutine이 발전하면서 Flow가 등장하게되었고 LiveData를 대체할 수 있는 StateFlow와 SharedFlow도 등장하면서 자연스럽게 Flow로 넘어갈 수 있게 되었습니다! (후후~~소리질러~!~)

Flow → StateFlow vs SharedFlow

아래와 같은 Flow 특징을 잘 살펴보면 LiveData를 완벽하게 대체하기에는 어려움이 있는데요.

Flow는 상태가 없습니다. 그러므로 현재 값을 알지못합니다.

Flow는 안드로이드 생명주기에 대해 알지못합니다. 따라서 생명주기에 따른 다른 처리가 필요합니다.

Flow는 Cold Stream 방식으로, 연속해서 들어오는 데이터를 처리할 수 없고 collect할 때마다 flow가 재실행됩니다.

이런 한계점들을 보완하여 나온 것이 바로 StateFlow와 SharedFlow입니다.

StateFlow

항상 값을 가지고 있고 오직 한 가지 값만 가집니다. 그러므로 초기값이 존재해야합니다.

여러 개의 collector를 지원합니다. 즉 flow를 공유할 수 있습니다.

collector 수에 상관없이 항상 구독하는 것의 최신 값을 받습니다.

Hot Stream 방식으로 collect할 때마다 flow가 재실행되지 않습니다.

SharedFlow

값을 가지지 않으며, 초기값을 갖고있지 않아도 됩니다.

replayCache가 존재하는데, replay란 collect시 전달받을 이전 데이터의 개수이고 몇 개까지 값을 캐싱하고 있을지에 대해서는 인자로 정의할 수 있습니다.

Hot Stream 방식입니다.

파라미터로 relplay, extraBufferCapacity, onBufferOverflow를 받을 수 있습니다.

StateFlow는 사실 SharedFlow의 한 종류이고 LiveData에 가장 가깝다고 할 수 있습니다. 그리고 항상 값을 가져야하는 StateFlow의 경우 UI 상태를 View에 노출시킬 때 효과적으로 사용할 수 있고 값을 갖고 있지 않아도 되는 SharedFlow의 경우 emit시 바로 collect되기 때문에 (왠지) 클릭 이벤트 등에 쓰면 효과적일 것 같습니다.

저는 이번 리팩토링에서 일단 StateFlow만 적용했기 때문에 StateFlow에 대해 더 자세히 적어보도록 하겠습니다. StateFlow가 LiveData를 대체할 수 있다해도 LiveData와 명백히 다른 점들도 있긴 있는데요. (허허) 그럼 과연 StateFlow를 어떻게 사용할 수 있는지 아래 예제를 통해 살펴보도록 하겠습니다.

img_2.png

StateFlow 그거 어떻게 하는건데

어떻게.jpeg

예제를 봅시다.

OliveViewModel.kt

@HiltViewModel
class OliveViewModel @Inject constructor(
        private val testUseCase: TestUseCase
) : ViewModel() {

  private val _testUIState = MutableStateFlow<UIState<Test>>(UIState.Empty)
  val testUIState: StateFlow<UIState<Test>> = _testUIState

  private fun getTest() {
    viewModelScope.launch {
      testUseCase()
              .onStart { _testUIState.update { UIState.Loading } }
              .catch { e -> _testUIState.update { UIState.Error(e) } }
              .collectLatest { value -> _testUIState.update { UIState.Success(value) } }
    }
  }
}

ViewModel 안에서 사용할 때는 LiveData와 그다지 차이가 없어보이는데요. StateFlow 말 그대로 state를 계속 update하고 지정한 flow에게 collect한 값을 보내준다고 생각하면 됩니다. collect / collectLatest하는 순간 최신의 값을 받고 있습니다.

  • StateIn을 사용하여 Flow를 StateFlow로 변환하기

    flow
        .stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(1000), initialValue = UIState.Empty)
    • scope : StateFlow가 Flow로부터 데이터를 받을 CoroutineScope
    • started : Flow로부터 언제부터 데이터를 받을지, 멈출지 명시
    • initialValue : StateFlow로 저장될 초기값


OliveActivity.kt
@AndroidEntryPoint
class OliveActivity {
  private val oliveViewModel: OliveViewModel by viewModels()

  lifecycleScope.launchWhenStarted {
    oliveViewModel.testUIState.collectLatest { state ->
      when (state) {
        is UIState.Loading -> ...
        is UIState.Success -> ...
        is UIState.Error -> ...
      }
    }
  }
}

Activity나 Fragment에서 사용할 때 주의깊게 봐야할 키워드가 있습니다. LiveData의 장점이자 Flow의 한계점인 LifeCycle를 인식하지 못한다는 점을 위해 위 예제에서는 launchWhenStarted를 사용했습니다. 그 밖에도 launchWhenResumed, launchWhenCreated, repeatOnLifeCycle이 있는데 모두 LifeCycle 상태에 따라서 collect할지, 중단할지 설정할 수 있습니다!

대신 repeatOnLifeCycle은 생명주기에 따라서 시작 > 정지 > 재시작을 하지만 launchWhen은 단 한번만 실행되어 재시작되지 않습니다.

무엇이 좋았나요?

일단 Flow를 적용하면서 클린아키텍처 관점에서 LiveData가 가진 한계점을 넘어봤다는 점이 좋았습니다. 처음 써본거라 잘못 사용하기도해서 예제 코드들도 보고 이론 공부도 하면서 무언가 알게된다는 기쁨이 생겨 뿌듯했습니다. 혼자 작게 생각해본거지만 Flow가 가진 장점들을 기반으로 더 다양한 API들이 개발이된다면 저 먼 미래에서 LiveData가 Deprecated되는 날도 있지않을까요? ㅎㅎ.. (언제까지 공부하죠?..)

다소 긴 글이었지만 읽어주셔서 감사합니다~! 다음에 또 좋은 주제로 (내년쯤) 찾아올게요..~!

루피.jpeg

AndroidKotlinStateFlow
올리브영 테크 블로그 작성 Android Kotlin StateFlow 도입기
💪
의지수 |
Android App Engineer
의지로 앱개발을 하고있습니다 :)