Android MVVM 이벤트 처리방법
업데이트:
카테고리: Android
/태그: Flow, LiveData, Sealed Class
lifeCycle이 onStop() 일 때 변경사항이 생긴다면 이벤트를 수신하지 못함. SharedFlow는 수신자와 상관없이 데이터를 내보내기 때문에 버퍼가 설정되어 있지 않다면 소실되어 버림https://medium.com/prnd/mvvm의-viewmodel에서-이벤트를-처리하는-방법-6가지-31bb183a88ce
한번만 트리거 되고 처리할 필요가 없는 이벤트의 경우 이전에는 Boolean으로 설정하여 조건에 맞게 하나하나 처리문을 걸어놓았고, 이렇게 하니 문제가 예상치 못한 부분에서 조건문이 통과되지 않아 정상적으로 작동되지 않는다는 것이였다.
-
LiveData + Event
View
— observe →ViewModel [ LiveData ]
ex ) A 액티비티에서 버튼을 클릭하여 B 액티비티로 이동하는 트리거가 있다. 이동할 때 토스트 메세지를 띄우는데 이때
Boolean
으로 해당 액션을 관리한다.이 때, B 액티비티에서 액션을 다 한뒤 A 액티비티로 돌아온다면 이전에 남아있는 LiveData의 값으로 인해서 불필요한 토스트 메세지를 팝업하게 된다.
Event Wrapper를 사용하여 한번만 처리하도록 설정한다.
// Event.kt open class Event<out T> (private val content: T) { // 외부에서는 읽을 수만 있도록 설정함 var hasBeenHandled = false private set /* 값이 한번 처리되었다면 null 아니라면 Handled 상태를 바꾸고 Content를 return */ fun getContentIfNotHandled() { return if (hasBeenHandled) { null } else { hasbeenHandled = true content } } // 처리 여부와 상관없이 content 반환 fun peekContent(): T = content }
viewModel 에서는
LiveData<Event<String>>
의 형태로 받아서 사용// Activity class Activity(): AppCompatActivity() { private val viewModel: ViewModel by viewModels() override fun onCreate() { super.onCreate() // Event 타입을 만들어 한번 처리후에는 로직이 돌아가지 않도록 viewModel.showToastEvent.observe(this, { event -> event.getContentIfNotHandled()?.let { text -> Toast.makeText(this, text, Toast.LENGTH_SHORT).show() } } } }
-
SingleLiveData
커스텀으로 LiveData + Event 타입을 제작
class SingleLiveData<T>: MutableLiveData<T>() { private val liveData = MutableLiveData<Event<T>>() protected constructor (value: T) { liveData.value = Event(value) } protected open fun setValue(value: T) { liveData.value = Event(value) } protected open fun postValue(value: T) { liveData.postValue(Event(value)) } // liveData는 현재 Event로 감싸져서 들어있기 때문에 fun getValue(): T = liveData.value.peekContent() fun observe(owner: LifeCycleOwner, onResult: (T) -> Unit) { liveData.observe(owner) { it.getContentIfNotHandled()?.let(onResult) } } fun observePeek(owner: LifeCycleOwner, onResult: (T) -> Unit) { liveData.observe(owner) { onResult(it.getPeekContent()) } } }
여러개의 옵저버를 등록하는 행위는 권장되지 않기에 여러개를 등록해서 사용하려면 Event Wrapper를 권장
등록한 관찰자에 따라 lifeCycle이 다르고 만약 비활성화 되어있는 상태에서 값이 변경된다면 활성화 되자마자 Event가 처리되어 원하지 않는 동작이 발생할 수 있음
-
SharedFlow
LiveData는 lifeCycle와 연관되어 사용되기 때문에 도메인 레이어에서는 사용하기가 힘들다.
-
StateFlow
: 현재 상태와 새로운 상태를 업데이트하여 Flow에 내보낸다.중간에 신규로 구독하면 최신상태를 반환한다.
UI에서 직접 업데이트를 하는 경우는
repeatOnLifeCycle
을 사용하여 화면이 보여지고 있을 때 변경이 일어나도록 진행값이 중복으로 방출되는 경우 값을 수집하지 않음
-
SharedFlow
: StateFlow와 유사하나 상태, Flow가 가지고 있는 값에 집중했다면 SharedFlow는 이벤트 처리 결과에 대한 값을 보내준다.replay
: 새로운 구독자에게 상태값 전달 여부, 적혀진 만큼 이전의 값을 보낸다.extraBufferCapacity
: 추가로 생성할 버퍼OnBufferOverflow
: 버퍼의 용량이 다 찼을 때 적용할 정책- 버퍼의 값이 유지되는 경우는 구독자가 값을 처리하지 않을 때
에러 핸들링(
tryEmit()
), 이전값 되돌리기(stateFlow는 이전의 값을 덮어씌우는 방식으로 업데이트), 알림 용도(stateFlow는 한명의 구독자, sharedFlow 다중 구독자 / 동일한 값을 재방출하는 형태)로 유용하다
-
-
SharedFlow + Sealed Class
Event를 관리하는 하나의 Flow만 정의하고, 관련이벤트는 해당 Flow에서 일괄 처리한다.
// ViewModel class SharedViewModel(): ViewModel() { private val _eventFlow = MutableSharedFlow<Event>() val eventFlow = _eventFlow.asSharedFlow() fun showToast() { event(Event.ShowToast("토스트")) } fun aaa() { event(Event.Aaa("aaa")) } fun bbb() { event(Event.Bbb(16)) } private fun event(event: Event) { viewModelScope.launch { _eventFlow.emit(event) } } // sealed class로 표현한 것은 상속 받은 자식이 누구인지 알 수 있어 // when과 같은 분기 표현을 할 때 명시적으로 표현할 수 있다. sealed class Event { data class ShowToast(val text: String): Event() data class Aaa(val value: String): Event() data class Bbb(val value: Int): Event() } }
// activity class Activity(): AppCompatActivity() { ... override fun onCreate() { super.onCreate() lifeCycleScope.launch { viewModel.eventFlow.collect { handleAction(it) } } } private fun handleAction(event: Event) { when (event) { is Event.ShowToast -> { Toast.makeToast(this, event.geekContent(), Toast.LENGTH_SHORT).show() } is Event.Aaa -> { Toast.makeToast(this, "aaa event: ${event.geekContent()}", Toast.LENGTH_SHORT).show() } is Event.Bbb -> { Toast.makeToast(this, "bbb event: ${event.geekContent()}", Toast.LENGTH_SHORT).show() } } } }
트리거되는 이벤트는 3개이지만, 하나의 eventFlow만 구독하여도 모두 처리가 가능하다
-
SharedFlow + Sealed class + LifeCycle
위와 같이 lifeCycleScope에 아무것도 걸지 않으면 화면이 내려가 있어도 동작을 하기때문에 메모리 누수가 날 수 있다.
그렇기에 화면이 Pause → Stop / Resume → Start 하여 필요할 때만 관리
repeatOnLifeCycle()
로 감싸주기만 하면 자동으로 collect / cancle을 실행lifeCycleScope.launch { repeatOnLifeCycle(Lifecycle.State.STARED) { viewModel.eventFlow.collect { handleAction(it) } } }
-
EventFlow + Sealed class + LifeCycle
lifeCycle이 onStop() 일 때 변경사항이 생긴다면 이벤트를 수신하지 못함. SharedFlow는 수신자와 상관없이 데이터를 내보내기 때문에 버퍼가 설정되어 있지 않다면 소실되어 버림
https://medium.com/prnd/mvvm의-viewmodel에서-이벤트를-처리하는-방법-6가지-31bb183a88ce
위의 링크에서는 따로 클래스를 제작하여 처리하는데 굳이 저럴필요가 있는가?