[Android] LiveData에서 StateFlow로 이전하기

sana·2022년 8월 21일
2

개요

Android Jetpack 라이브러리 중 하나인 LiveData는 관찰 가능한 데이터 홀드 클래스로 앱의 UI 상태를 업데이트하는 데 많이 써 왔다. 하지만, 최근 코루틴의 Flow API 중 하나로 StateFlow가 등장하여 LiveData 를 대체하고 있다.
이 글에서는 어떻게 LiveDataStateFlow로 대체할 수 있는지 알아보도록 하겠다.


LiveData의 한계와 StateFlow의 등장

LiveData는 Activity, Fragment, Service 등 안드로이드 앱 컴포넌트의 생명 주기를 인식하여 메모리 누수 없이 데이터 관리를 해준다는 장점이 있다.

그러나 LiveData에는 다음과 같은 한계가 있다.

  • 비동기 스트림을 지원하지 않는다.
    LiveData는 UI와 밀접하게 연관되어 있어 오직 메인스레드(Main Thread)에서만 읽고 쓸 수 있다. 따라서 ViewModel에서 LiveData를 사용하여 View를 업데이트할 때는 사용할 수 있지만, Data Layer에서 데이터를 처리할 때는 사용하기 어렵다. 데이터를 I/O 할 때에는 메인스레드(Main Thread)가 아닌 작업스레드(Worker Thread)에서 비동기 방식으로 처리되어야 하기 때문이다.

  • 안드로이드 플랫폼에 종속적이다.
    LiveData가 안드로이드 생명주기를 인식한다는 점은, Clean Architecture 관점에서 보았을 때 단점이 될 수도 있다. Presentation Layer에서는 편리하게 사용할 수 있지만, 플랫폼에 종속적이지 않은 순수한 Kotlin 및 Java 코드로 이루어져야 하는 Domain Layer(Business Layer)에서는 사용하기 어렵기 때문이다.

위와 같은 LiveData의 한계를 극복하여 대신 사용할 수 있는 것이 Flow이다.

Flow는 값을 순차적으로 방출(emit)하고 정상적 혹은 예외적으로 완료되는 비동기 데이터 스트림이기 때문에 Data Layer에서 쓸 수 있다. 또한, 코틀린의 코루틴 API이기 때문에 안드로이드 플랫폼에 종속적이지도 않다.

다만, ViewModel에서는 Flow를 직접 쓰는 것이 아니라 StateFlow를 사용하고, View에서는 Lifecycle.repeatOnLifecycle 블록에서 이를 수집하여 사용하여 LiveData처럼 안드로이드의 생명주기를 직접 인식할 수 없는 문제를 해결한다.


StateFlow의 특징

StateFlow는 Flow API 중 하나로 현재 상태와 새로운 상태 업데이트를 collector에 내보내는 관찰 가능한 State Holder Flow이다. LiveData와 마찬가지로 value 속성을 통해 현재 상태 값을 읽을 수 있다.

StateFlow의 특징은 다음과 같다.

  • Hot stream이기 때문에 생성하자마자 바로 활성화되며, 값이 업데이트 된 경우에만 반환한다.

  • 항상 한 개의 값을 가지고 있기 때문에 초기 상태를 생성자에게 전달해야 한다.

  • 여러 개의 collector 를 지원하기 때문에 중복 리소스 요청을 방지한다.

  • collector 수에 관계없이 항상 구독하고 있는 것의 최신 값을 받는다.



LiveData를 StateFlow로 대체

그렇다면 간단한 노트 앱을 만든다고 가정하여 실제 코드를 살펴보자.

NotesViewModel.kt

NotesViewModel에서는 다음 두 가지 일을 하고 있다.

  • NoteRepository에 저장된 노트 목록을 가져와 보여줌 (loadData)
  • 노트 상세 화면으로 이동하는 이벤트를 전달함 (openDetails)

첫 번째 경우는 뷰의 상태를 처리한다. 따라서 화면 구성이 변경되었을 때도 마지막으로 방출된 노트 목록 값이 수신되어 보여져야 한다. 두 번째 경우는 화면이 이동되는 일회성 이벤트로 값을 오직 한 번만 받는다.

LiveData 활용

LiveData를 활용하면 다음과 같이 구현할 수 있다.

class NotesViewModel(
    private val noteRepository: NoteRepository
) : ViewModel() {

    private val _uiState = MutableLiveData<UiState<List<Note>>>()
    val uiState: LiveData<UiState<List<Note>>> = _uiState

    private val _navigationEvent = SingleLiveEvent<NavigationEvent>()
    val navigationEvent: SingleLiveEvent<NavigationEvent> = _navigationEvent
    
    fun loadData() {
        _uiState.value = UiState.Loading
        viewModelScope.launch {
            noteRepository.getAllNotes().collect {
                _uiState.value = UiState.Success(it)
            }
        }
    }

    fun openDetails() {
        _navigationEvent.value = NavigationEvent.ToDetails
    }
  

}
  

우선 loadData() 메소드를 살펴보면, NoteRepository에서 반환한 Flow 객체를 collect하여 MutableLiveData인 _uiState 객체에 value를 넣어주고 있다.

그리고 뷰에서 노트 아이템 하나를 클릭했을 때 openDetails() 가 호출된다. 이 때 상세 화면으로 이동하는 이벤트가 뷰로 전달되어야 한다. 그런데 MutableLiveData를 쓰면 명시적으로 발행된 이벤트만 전달되는 것이 아니라 이전에 발행되었던 이벤트까지 전달될 수 있기 때문에 MutableLiveData를 확장하여 만든 SingleLiveEvent 클래스를 만들어 화면 이동 처리를 해준다.


flow 활용


class NotesViewModel(
    private val noteRepository: NoteRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<UiState<List<Note>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<Note>>> = _uiState.asStateFlow()
  
    private val _navigationEvent = MutableSharedFlow<NavigationEvent>(replay = 0)
    val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent

    fun loadData() {
        viewModelScope.launch {
            noteRepository.getAllNotes().collect {
                  _uiState.emit(UiState.Success(it))
            }
        }
    }
    
    fun openDetails() {
        viewModelScope.launch {
            _navigationEvent.emit(NavigationEvent.ToDetails)
        }
    }

}
  

LiveData와 달리 StateFlow에는 초기 값이 필요하다. 따라서 _uiState 객체를 생성할 때 UiState.Loading을 초기값으로 한다. 또한 flow 빌더에서 emit() 함수를 통해 NoteRepository에서 수집한 데이터를 방출하고 있다.

또한 이벤트 처리의 경우 SingleLiveEvent 클래스를 따로 만들어 줄 필요 없이 SharedFlow를 사용하여 손쉽게 구현할 수 있다. SharedFlowStateFlow와 달리 초기값을 가지지 않고, replay 매개변수를 통해 값을 수집할 수 있는 횟수를 결정할 수 있다. 여기에서는 replay=0 으로 설정하여 클릭이벤트가 발생했을 때 데이터가 한 번만 수집되도록 하였다. 따라서 화면 회전 등 앱의 구성이 변경해도 이벤트는 다시 발생하지 않는다.



NotesFrgament.kt

LiveData 활용

class NotesFragment : Fragment(R.layout.fragment_notes) {

    private val notesViewModel: NotesViewModel by viewModels()

    private fun observeData() {

        notesViewModel.uiState.observe(viewLifecycleOwner) {
            handleUiState(it)
        }

        notesViewModel.navigationEvent.observe(viewLifecycleOwner) {
            handleEvent(it)
        }
    }

}

NotesFragment에서는 observe를 통해 ViewModel의 LiveData를 관찰하여 UI를 업데이트 해준다.


flow 활용

class NotesFragment : Fragment(R.layout.fragment_notes) {

  private val notesViewModel: NotesViewModel by viewModels()

  private fun observeData() {
      viewLifecycleOwner.lifecycleScope.launch {
          notesViewModel.uiState.collect {
              handleUiState(it)
          }
      }

	    viewLifecycleOwner.lifecycleScope.launch {
          notesViewModel.navigationEvent.collect {
              handleEvent(it)
          }
      }
  }

}

뷰의 생명주기를 인식하는 LiveData와 달리 Flow는 생명주기를 알지 못한다. 따라서 뷰가 STOPPED 상태가 되면 LiveData.observe()는 소비자 등록을 자동으로 취소하는 반면, StateFlow는 수집을 자동으로 중지하지 않는다. 따라서 lifeCycleScope 확장함수를 통해 생명주기에 따라 변경된 값을 collect하여 UI를 업데이트 해줄 수 있도록 한다.

SharedFlow의 경우도 마찬가지로 collect로 방출된 결과를 받아온다.



마치며

지금까지 LiveData의 한계와 StateFlow의 특징을 살펴보고 예제를 통해 LiveData를 StateFlow로 대체하는 방법을 살펴 보았다. 둘 모두 관찰 가능한 데이터 홀더 클래스이며, MVVM 아키텍처 패턴에서 비슷하게 사용할 수 있기 때문에 어렵지 않게 프로젝트에 StateFlow를 적용할 수 있을 것이다.



[참고자료]

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ko
https://alexzh.com/migrate-from-livedata-to-stateflow-and-sharedflow/
https://yjyoon-dev.github.io/android/2022/02/12/android-02/

0개의 댓글