업데이트:

카테고리:

/

태그: , ,

lifeCycle이 onStop() 일 때 변경사항이 생긴다면 이벤트를 수신하지 못함. SharedFlow는 수신자와 상관없이 데이터를 내보내기 때문에 버퍼가 설정되어 있지 않다면 소실되어 버림https://medium.com/prnd/mvvm의-viewmodel에서-이벤트를-처리하는-방법-6가지-31bb183a88ce

한번만 트리거 되고 처리할 필요가 없는 이벤트의 경우 이전에는 Boolean으로 설정하여 조건에 맞게 하나하나 처리문을 걸어놓았고, 이렇게 하니 문제가 예상치 못한 부분에서 조건문이 통과되지 않아 정상적으로 작동되지 않는다는 것이였다.

  1. 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()
     			}
     		}		
     	}
     }
    
  2. 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가 처리되어 원하지 않는 동작이 발생할 수 있음

  3. SharedFlow

    LiveData는 lifeCycle와 연관되어 사용되기 때문에 도메인 레이어에서는 사용하기가 힘들다.

    • StateFlow : 현재 상태와 새로운 상태를 업데이트하여 Flow에 내보낸다.

      중간에 신규로 구독하면 최신상태를 반환한다.

      UI에서 직접 업데이트를 하는 경우는 repeatOnLifeCycle 을 사용하여 화면이 보여지고 있을 때 변경이 일어나도록 진행

      값이 중복으로 방출되는 경우 값을 수집하지 않음

    • SharedFlow : StateFlow와 유사하나 상태, Flow가 가지고 있는 값에 집중했다면 SharedFlow는 이벤트 처리 결과에 대한 값을 보내준다.

      • replay : 새로운 구독자에게 상태값 전달 여부, 적혀진 만큼 이전의 값을 보낸다.
      • extraBufferCapacity : 추가로 생성할 버퍼
      • OnBufferOverflow : 버퍼의 용량이 다 찼을 때 적용할 정책
      • 버퍼의 값이 유지되는 경우는 구독자가 값을 처리하지 않을 때

      에러 핸들링(tryEmit()), 이전값 되돌리기(stateFlow는 이전의 값을 덮어씌우는 방식으로 업데이트), 알림 용도(stateFlow는 한명의 구독자, sharedFlow 다중 구독자 / 동일한 값을 재방출하는 형태)로 유용하다

  4. 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만 구독하여도 모두 처리가 가능하다

  5. SharedFlow + Sealed class + LifeCycle

    위와 같이 lifeCycleScope에 아무것도 걸지 않으면 화면이 내려가 있어도 동작을 하기때문에 메모리 누수가 날 수 있다.

    그렇기에 화면이 Pause → Stop / Resume → Start 하여 필요할 때만 관리

    repeatOnLifeCycle() 로 감싸주기만 하면 자동으로 collect / cancle을 실행

     lifeCycleScope.launch {
     	repeatOnLifeCycle(Lifecycle.State.STARED) {
     		viewModel.eventFlow.collect { handleAction(it) }
     	}
     }
    
  6. EventFlow + Sealed class + LifeCycle

    lifeCycle이 onStop() 일 때 변경사항이 생긴다면 이벤트를 수신하지 못함. SharedFlow는 수신자와 상관없이 데이터를 내보내기 때문에 버퍼가 설정되어 있지 않다면 소실되어 버림

    https://medium.com/prnd/mvvm의-viewmodel에서-이벤트를-처리하는-방법-6가지-31bb183a88ce

    위의 링크에서는 따로 클래스를 제작하여 처리하는데 굳이 저럴필요가 있는가?