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 전달하기
참고 문서
'Android > 이론' 카테고리의 다른 글
[안드로이드] MediatorLiveData를 이용하여 여러개의 LiveData를 하나의 LiveData로 합치기 (0) | 2021.02.26 |
---|---|
[안드로이드 Kotlin] Room Database, Repository, ViewModel을 Koin으로 의존성 주입(DI) (0) | 2021.01.11 |
[Android Test] Espresso @SmallTest, @MediumTest, @LargeTest 구분의 의미 (0) | 2020.08.18 |
Kotlin 문법 (2) 데이터 클래스(Data class) 사용하기 (0) | 2020.07.17 |