지인에게 왜 화면 회전을 시키면 한번 띄워졌던 토스트메시지 혹은 Dialog가 왜 다시 띄워지는지 모르겠다는 질문을 받았습니다.
그 지인의 ViewModel 부분 코드입니다.
액티비티에서는 아래의 라이브데이터들을 observe하고 있습니다.
val showDialog = MutableLiveData<Boolean>(false)
val msg = MutableLiveData<String>()
어떤점이 문제였을까요??
문제가 발생한 이유 3가지
1. AAC ViewModel은 Activity의 모든 생명주기와 함께한다.
AAC ViewModel은 Activity의 UI와 관련한 데이터를 보관하기 위해 설계되었습니다. 그러므로 Activity가 finish()되어서 ViewModel의 onCleared()가 불리지 않는 한 Activity의 생명주기 동안 ViewModel은 살아있습니다.
2. LiveData를 사용하였다.
LiveData는 ViewModel Scope 동안 저장된 데이터를 계속 가지고 있습니다. Activity가 onStop(), onPause() 될 때에도 LiveData의 상태만 변경될 뿐 값은 계속 가지고 있게 됩니다.
3. LiveData의 상태가 InActive -> Active 상태로 바뀌었다.
일반적으로 LiveData는 값이 변경될 때만 옵저버에게 콜백을 전달하지만, 예외적으로 observer가 inactive -> active 상태로 변경될 때도 콜백을 전달합니다.
문제점이 발생하게 된 과정을 적어보면
- Activity의 onCreate()에서 LiveData를 구독
- LiveData 값 변경된다.
- LiveData 값의 변경으로 인해 토스트 혹은 다이얼로그 출력한다.
- 화면 회전 (onPause -> onStop -> onDestroy -> onCreate -> onStart -> onResume)
- Activity의 onCreate()에서 다시 LiveData를 구독
- LiveData의 상태가 InActive -> Active로 바뀌었으므로 옵저버가 다시 이전에 저장한 값으로 콜백을 호출한다.
- 토스트 혹은 다이얼로그를 출력한다.
LiveData는 기본적으로 Observe하는 컴포넌트(Activity, Fragment)의 LifeCycle에 따라서 UI관련 데이터들이 기기 회전 등이 일어나도 항상 최신데이터를 유지할수 있게끔 만들어졌으므로 LiveData에 단일 이벤트를 저장해서 사용 할 경우 위와 같은 과정이 일어나게 되어 문제가 발생하게 됩니다.
어떻게 해결 할까요??
여러 방법이 있겠지만 대표적으로 두가지 해결책이 있습니다.
1. SingleLiveEvent
2. EventWrapper (권장)
이 두가지 중 하나를 사용하시면 됩니다.
1. SingleLiveEvent 사용 예시
코드
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
@MainThread
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
@MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveEvent"
}
}
사용법
ViewModel.kt
private val _fabClickEvent = SingleLiveEvent<Unit>()
val fabClickEvent: LiveData<Unit>
get() = _fabClickEvent
fun onFabClick() {
_fabClickEvent.call()
}
Activity.kt
vm.fabClickEvent.observe(viewLifecycleOwner, Observer {
animateFab()
})
하지만 SingleLiveEvent 코드를 보면 알수 있듯이 여러개의 옵저버를 등록할 수 없도록 설계되었습니다. 만약 여러개의 옵저버가 구독한다면 어떤 것이 실행 될지는 보장되지 않습니다. 그래서 Event Wrapper 클래스가 권장됩니다.
2. Event Wrapper 사용 예시 (권장)
코드
Event.kt
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) { // 이벤트가 이미 처리 되었다면
null // null을 반환하고,
} else { // 그렇지 않다면
hasBeenHandled = true // 이벤트가 처리되었다고 표시한 후에
content // 값을 반환합니다.
}
}
/**
* 이벤트의 처리 여부에 상관 없이 값을 반환합니다.
*/
fun peekContent(): T = content
}
EventObserver.kt
import androidx.lifecycle.Observer
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
사용법
ViewModel.kt
private val _callEvent = MutableLiveData<Event<String>>()
val callEvent: LiveData<Event<String>>
get() = _callEvent
fun callEvent(phone: String) {
val phoneNumber = phone.filter { it in "123456789" }
_callEvent.value = Event(phoneNumber)
}
Activity.kt
callEvent.observe(viewLifecycleOwner, EventObserver(this@DetailFragment::startCallApp))
private fun startCallApp(phone: String) {
startActivity(Intent(Intent.ACTION_DIAL, "tel:$phone".toUri()))
}
요약
단일 이벤트의 실행은 SingleLiveEvent 혹은 Event Wrapper 클래스를 사용하자