[Android] Event Wrapper를 사용한 단일 이벤트 처리
Android

[Android] Event Wrapper를 사용한 단일 이벤트 처리

728x90

 

 

MVVM 패턴에서 ViewModel은 View에 대한 직접적인 참조를 할 수 없습니다. 이때 ViewModel에서 View의 이벤트를 전달해주기 위해선 어떻게 해야 할까요?

 

 간단한 예를 들어보겠습니다. ViewModel에서 데이터를 받아왔는데 에러가 생기고 말았습니다. 에러에 대한 메시지를 사용자에게 토스트로 보여주고 싶습니다. 토스트를 화면에 보여주는 동작은 View의 역할입니다. 그럼 이때 ViewModel에서 View한테 에러가 났다는 이벤트를 전달해주어야 합니다.

 MVVM 패턴에서는 보통 ViewModel에서 LiveData를 선언한 뒤 View에서 LiveData를 옵저빙 하는 방식으로 많이 사용합니다. 위의 예시를 LiveData를 사용해서 구현해보겠습니다.

class SampleViewModel(private val sampleRepository: SampleRepository) : ViewModel() {
    
    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error
    
    init {
        viewModelScope.launch {
            sampleRepository.loadSample().collect {
                it.onFailure(::handleError)
            }    
        }
    }
    
    private fun handleError(exception: Throwable) {
        _error.value = exception.message
    }
}

 Repository의 loadSample() 메서드를 사용해 데이터를 가져오는데 에러가 발생하는 상황입니다. 에러가 발생하면 MutableLiveData에 에러 메시지를 담아줍니다.

 

viewModel.error.observe(this) { message ->
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

 Activity나 Fragment에서 ViewModel에서 error를 옵저빙 하여 이벤트가 변경되면 토스트를 보여줍니다.

 

 

 위와 같이 코드를 작성하여 동작하면 에러가 발생했을 때 정상적으로 에러 메시지가 보여주는 것을 확인할 수 있습니다. 하지만 이 코드에는 문제가 있습니다. 화면 회전 같은 동작이 일어나게 되면 토스트가 한번 더 나오게 됩니다. 이와 같은 현상은 왜 일어나게 될까요? 답은 ViewModel의 생명주기에 있습니다.

https://developer.android.com/topic/libraries/architecture/viewmodel

 

 MVVM 패턴을 공부하신 분들이라면 위의 사진을 많이 보셨을 겁니다. 화면 회전과 같은 동작이 일어나게 되면 Activity는 파괴된 후 다시 생성하게 됩니다. 그에 반해 ViewModel은 파괴되지 않고 그대로 살아있기 때문에 ViewModel 안에 있는 데이터들은 그대로 유지됩니다.

 이 때문에 Activity가 다시 생성되고 ViewModel 안에 있는 LiveData를 다시 한번 옵저빙 하게 되면서 기존에 가지고 있던 에러 메시지를 다시 한번 보여주게 됩니다. 에러 메시지를 한번 보여줬었는데 또다시 보여줄 필요는 없겠죠? 위의 코드를 개선해보도록 하겠습니다.

 

 

Event Wrapper

 1회성으로만 이벤트를 전달해주기 위해서는 별도의 처리 과정이 필요합니다. View에게 '이건 단일 이벤트야'라고 알려주기 위해서는 데이터를 Event Wrapper로 감싸주면 됩니다.

open class Event<out T>(private val content: T) {
    var hasBeenHandled = false
        private set

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    fun peekContent(): T = content
}

class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
    override fun onChanged(event: Event<T>?) {
        event?.getContentIfNotHandled()?.let { value ->
            onEventUnhandledContent(value)
        }
    }
}

 Event Wrapper에는 단일 이벤트가 이미 실행됐는지를 판단하는 hasBeenHandled 변수가 포함되어 있습니다. 초기값은 false였다가 단일 이벤트가 실행되면 hasBeenHandled 값을 true로 변경해줍니다.

 Event Wrapper를 Activity에서 옵저빙 할 때는 일반 Observer가 아닌 EventObserver를 사용해서 옵저빙 해줍니다. EventObserver에서는 Event의 getContentIfNotHandled() 메서드를 사용해서 기존에 단일 이벤트가 실행되지 않은 경우에만 콜백 메서드를 실행시킵니다.

 

 

 아래는 Event Wrapper를 사용해서 개선시킨 ViewModel입니다.

class SampleViewModel(private val sampleRepository: SampleRepository) : ViewModel() {

    private val _error = MutableLiveData<Event<String>>()
    val error: LiveData<Event<String>> = _error

    init {
        viewModelScope.launch {
            sampleRepository.loadSample().collect {
                it.onFailure(::handleError)
            }
        }
    }

    private fun handleError(exception: Throwable) {
        val message = exception.message ?: ""
        _error.value = Event(message)
    }
}

 에러 메시지를 Event Wrapper로 감싼 뒤 LiveData에 값을 설정해줄 때도 Event Wrapper 그대로 설정해줍니다.

viewModel.error.observe(this, EventObserver { message ->
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
})

 Activity나 Fragment에서 이벤트를 옵저빙 할 때는 EventObserver를 사용해서 옵저빙 해줍니다. 

 

 

끝으로

 ViewModel에서 View에게 단일 이벤트를 전달해주기 위한 Event Wrapper를 알아보았습니다. 몇몇의 예제에서 이벤트 전달을 위한 콜백 인터페이스를 만들어서 사용하는 것을 봤는데, 이렇게 되면 Activity와 ViewModel 사이의 불필요한 연결고리가 생기기 때문에 좋은 방법은 아니라고 생각합니다. MVVM 패턴에서는 ViewModel의 이벤트를 구독하고 있다가 값이 변경되는 이벤트가 발생하면 처리하는 방식이 정석적이기 때문에 Event Wrapper를 활용하면 좋을 것 같습니다.

728x90