우찬쓰 개발블로그
Event용 LiveData 적용하기 본문
MVVM 아키텍에서 LiveData를 쓰다보면 흔하게 마주할 수 있는 상황이 바로 Event 일회성 처리에 대한 문제이다.
private val _eventStartSettingActivity = MutableLiveData<Unit>()
val eventStartSettingActivity: LiveData<Unit> get() = _eventStartSettingActivity
MainActivity로부터 SettingActivity를 시작하는 다음과 같은 event용 LiveData가 있다고 해보자.
이 이벤트를 emit하는 곳은 activity_main.xml에서 onClick을 처리하도록한 databinding이라고 생각하고, 이 클릭이벤트의 대한 메소드를 아래와 같이 정의했다.
fun onClickSettingMenu() {
_eventStartSettingActivity.postValue(Unit)
}
그리고 MainActivity에서 observe하여 이 이벤트에 대한 구독 처리를 했다고 생각해보자.
viewModel.eventStartSettingActivity.observe(this, {
startActivity(Intent(this, SettingActivity::class.java))
})
이러한 코드의 문제점은 Activity가 재성성되고 viewModel은 그대로인 상황(화면 회전, 다크모드 변경, 언어 변경 등)의 상황에서 LiveData는 데이터를 가지고 있고, 구독만 새로하게 되기 때문에 즉시 다시 이벤트가 발생한다는 점이다.
그래서 이런 방식의 처리를 위한 방법이 여러가지 고안 되었는데, 그중 두개와 내가 선택한 하나를 소개하고자 한다.
1. SingleLiveData
public class SingleLiveEvent<T> extends MutableLiveData<T> {
private static final String TAG = "SingleLiveEvent";
private final AtomicBoolean mPending = new AtomicBoolean(false);
@MainThread
public void observe(LifecycleOwner owner, final Observer<T> observer) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
}
// Observe the internal MutableLiveData
super.observe(owner, new Observer<T>() {
@Override
public void onChanged(@Nullable T t) {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}
}
});
}
@MainThread
public void setValue(@Nullable T t) {
mPending.set(true);
super.setValue(t);
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
public void call() {
setValue(null);
}
}
SingleLiveData 방식으로 한번의 set에 한번의 이벤트를 보장한다.
이 방식의 단점은 하나의 LiveData에 여러개의 이벤트 구독자가 생길시 하나의 구독자에게만 이벤트가 가게된다는 점이다.
물론 그러한 단점을 개선한 다른 버전의 SingleLiveData도 출처에 소개되고 있다.
2. Event Wrapper
gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#file-event-kt
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
}
Event를 한번 Wrapping하는 방법이다.
이렇게하면 LiveData<Event<Value>>같은 형태로 이벤트 처리가 가능하다.
필자는 여기서 2번의 방식을 선택했다.
LiveData를 직접 커스텀한 클래스를 만드는것 보다는 보내고자 하는 Value를 통해 문제를 해결하는 쪽이 깔끔해 보였다.
그래서 위 소스를 참고하여 변형하여 만든 필자의 소스는 다음과 같다.
open class Event<T>(value: T) {
var value = value
private set
private var isAlreadyHandled = false
fun isActive(): Boolean = if (isAlreadyHandled) {
false
} else {
isAlreadyHandled = true
true
}
}
fun <T> LiveData<Event<T>>.observeEvent(owner: LifecycleOwner, observer: Observer<T>) = observe(owner) {
if (it.isActive()) {
observer.onChanged(it.value)
}
}
fun MutableLiveData<Event<Unit>>.emit() = postValue(Event(Unit))
fun <T> MutableLiveData<Event<T>>.emit(value: T) = postValue(Event(value))
이렇게 하면 실제로 사용할때 명확한 구분이 가능하다.
이 소스를 적용한 최종 소스를 보자.
private val _eventStartSettingActivity = MutableLiveData<Event<Unit>>()
val eventStartSettingActivity: LiveData<Event<Unit>> get() = _eventStartSettingActivity
fun onClickSettingMenu() {
_eventStartSettingActivity.emit()
}
viewModel.eventStartSettingActivity.observeEvent(this, {
startActivity(Intent(this, SettingActivity::class.java))
})
기존 코드의 사용법을 거의 고치지 않으면서도 Readability가 훨씬 좋아진 것을 확인할 수 있다.
(참고문헌)
'안드로이드 > 안드로이드 개발' 카테고리의 다른 글
Android WebView에서 element 존재 여부 체크 (0) | 2022.05.10 |
---|---|
안드로이드에서 Kotlin Coroutine으로 Event Bus 구현 (0) | 2021.04.11 |
쉽고 빠른 안드로이드 다크모드 적용기 (0) | 2021.04.08 |
안드로이드 앱 모바일 크롬 디버깅 (0) | 2021.03.03 |
clearFocus 후에도 focus가 제대로 안사라지는 이슈 (0) | 2021.02.22 |