본문 바로가기

Android/이론

ViewModel과 View(Activity, Fragment)간의 이벤트 처리― Event Wrapper 사용하기

728x90
반응형

 

 

ViewModel에서는 view를 참조할 수 없습니다. (하면 안됩니다.)

그래서 view를 참조하는 메서드는 Activity, Fragment 내에서 구현되게 되는데요.

ViewModel안에서 그 메서드들이 필요할 때, 또는 ViewModel에서 View에 이벤트를 보내주어야할 때!

LiveData를 이용해서 ViewModel의 변수 값을 변화시키고, 그 변수를 view에서 observer하는 방식으로 이벤트를 처리할 수 있습니다.

이것이 아주 간편한 방식이죠.

 

예를 들어, 텍스트를 입력하다 버튼 클릭 시 키보드를 숨겨야 하는 Fragment의 메서드 hideKeyboard() 메서드를 실행해야 합니다.

ViewModel에서 hideKeyboard 변수를 라이브데이터로 만들어주고, Fragment에서 옵저빙합니다.

class MytViewModel : ViewModel() {
    private val _hideKeyboard = MutableLiveData(false)
    val hideKeyboard: LiveData<Boolean>
        get() = _hideKeyboard

    fun onClickButton() {
        _hideKeyboard.value = true
    }

    fun hideKeyboardHandled() {
        _hideKeyboard.value = false
    }
}

 

위의 소스는 ViewModel에서 버튼 클릭시 hideKeyboard의 value를 true로 만들어 주고,

밑의 소스는 Fragment에서 뷰모델의 hideKeyboard 변수를 observe 하여 값이 true일 때다시 false로 만들어 주고, hideKeyboard() 메서드를 실행합니다.

viewModel.hideKeyboard.observe(this, Observer {
    if (it) { 
        viewModel.hideKeyboardHandled() 
        hideKeyboard()
    }
})

 

이러한 구현 방식은 이해하기 어렵고 복잡하다는 단점이 있습니다. 이벤트가 일어난 후 값을 일일이 리셋해야 합니다.

  • 이벤트가 일어난 후 일일히 값을 리셋하는 이유는, 단순히 값이 바뀌는 이벤트 뿐 아닌 생명주기나 옵저버의 상태(inactive → active등) 에 따라 이벤트가 계속 발생할 수 있기 때문입니다. 이것은 아주 흔한 일입니다. 명시적으로 값을 주지 않아도(post, set(value)) 이벤트가 여러번 들어와 나를 매우 고통스럽게합니다^^;;

 

이에 대한 대안으로 많이 사용하는 코드가 있습니다.

 

  • SingleLiveEvent

/**
 * A lifecycle-aware observable that sends only new updates after subscription, used for events like
 * navigation and Snackbar messages.
 * <p>
 * This avoids a common problem with events: on configuration change (like rotation) an update
 * can be emitted if the observer is active. This LiveData only calls the observable if there's an
 * explicit call to setValue() or call().
 * <p>
 * Note that only one observer is going to be notified of changes.
 */
class SingleLiveEvent<T> : MutableLiveData<T>() {
    private val isPending = AtomicBoolean(false)


    @MainThread
    override fun setValue(value: T?) {
        isPending.set(true)
        super.setValue(value)
    }

  
    // Observe the internal MutableLiveData
    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, Observer {
            if (isPending.compareAndSet(true, false)) {
                observer.onChanged(it)
            }
        })
    }

   /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }
}

 

적용은 밑과 같습니다.

class MyViewModel : ViewModel {
    private val _hideKeyboard = SingleLiveEvent<Void>()

    val navigateToDetails : LiveData<Void>
        get() = _hideKeyboard


    fun onClickButton() {
        _hideKeyboard.call()
    }
}

 

viewModel.hideKeyboard.observe(this, Observer {
    hideKeyboard()
})

 

그러나 SingleLiveEvent의 문제는 하나의 observer에 제한된다는 것입니다.

만약 부주의하게 observer를 더 추가했다면, 하나만 호출됩니다. (어느것일지 보장할 수는 없습니다.)

 

  • Event wrapper

그래서 Event wrapper를 사용하는것이 권장됩니다.

구글 샘플코드에서도 SingleLiveEvent를 사용하지 않고, Event wrapper를 대신 사용한 예제를 볼 수 있습니다.

이벤트 래퍼는 event가 처리되었는지 여부를 명시적으로 관리하여 실수를 줄여줍니다.

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

 

class MyViewModel : ViewModel {
    private val _hideKeyboard = MutableLiveData<Event<Boolean>>()

    val hideKeyboard : LiveData<Event<Boolean>>
        get() = _hideKeyboard


    fun onClickButton() {
        _hideKeyboard.value = Event(true)  // Trigger the event by setting a new Event as a new value
    }
}

 

viewModel.hideKeyboard.observe(this, Observer {
    it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
       hideKeyboard()
    }
})

 

이 방식의 장점은 사용자가 getContetnIfNotHandled() 또는 peekContent()을 사용하여 의도를 지정할 수 있다는 것입니다.

getContentIfNotHandled()를 사용하여 재사용을 막거나, peekContent()를 사용하여 이벤트 처리 여부에 상관 없이 값을 반환 할 수 있습니다.

peekContent() 를 사용한다면 위 그림과 같이 여러개의 observer를 사용할 수 있습니다.

SingleLiveEvent의 대안으로 Event wrapper가 사용되고 있지만,  SingleLiveEvent를 자신의 코드에 맞게 바꾸어 사용할 수도 있을 것입니다.

Event wrapper또한 자신의 코드에 맞게 커스터마이즈하여 사용해 볼 수 있습니다.

 

이를 통해 ViewModel뿐 아니라 Repository의 데이터를 관찰하여 이벤트 처리할 수 있습니다.

[Android/이론] - [안드로이드 MVVM] Repository 에서 ViewModel, View(Activity,Fragment)에 Event/Data 전달하기

 

참고 문서

 

 

 

 

728x90
반응형