[Android] LiveData를 1번만 관찰하는 방법
이번에 해결할 이슈는 LiveData의 value를 1번만 observing하는 것이다.
하고자 했던 것은 특정 기준을 달성하면 서버에서 보상에 대한 데이터를 전송해주고, 그 데이터에 대한 다이얼로그를 띄워야 했다.
하지만 LiveData로 보상 데이터를 observing하는 상태에서 해당 데이터를 다이얼로그로 띄우니
다이얼로그를 닫아도(dismiss) 계속해서 다이얼로그가 발생했다😶🌫️
LiveData를 사용한 이유?
우선 해결 방법 설명에 앞서, LiveData를 사용한 이유에 대해서 알아봐야 한다.
View가 ViewModel과 통신하는 편리한 방법은 Observable이 가능한 LiveData를 이용하는 것이다
→ View는 LiveData의 변경 사항을 subscribe하고 이에 반응하는 것
→ 따라서 화면에 연속적으로 표시되는 데이터에 적합하다
BUT snackbar, toast, navigation, dialog는 데이터를 1번만 사용해야 한다.
→ 스낵바나 토스트, 다이얼로그는 1번만 띄워지는 팝업이기 때문
→ 하지만 LiveData를 observing해서 그 값을 팝업에 사용하면 팝업이 연속적으로 표시되게 된다.
위의 블로그에서 방법을 찾을 수 있었다.
❌ 해결 방법 1 : 이벤트에 LiveData 사용하는 방법
이 방법은 내가 처음에 실제로 해보고 이건 안된다는 것을 깨달았던 방법인데.. 나쁜 방법으로 명시되어 있었다..ㅎㅎ
// Don't use this for events
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
}
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
→ navigateToDetails의 값을 observing해서 값이 true이면 DetailsActivity로 이동하는 것이다.
→ 다시 현재의 View로 돌아왔을 때, 이전에 true로 변경된 값이 보존되고 있기 때문에 다시 DetailsActivity로 전환되는 오류가 발생
당연히 다시 화면으로 돌아왔을 때 LiveData의 원래 값이 활성화 되기 때문에 적당한 방법이 아니다.
🤨 다시 현재 View로 돌아왔을 때, 즉시 LiveData의 값을 false로 설정한다면?
여기서 발생할 수 있는 2가지 문제점이 있다고 한다.
- LiveData가 값을 가지고는 있지만 수신한 모든 값을 내보낸다는 보장이 없다
- 무엇보다.. 코드 자체가 이해하기 어렵고 ugly하다
❌ 해결 방법 2 : 이벤트에 LiveData 사용하고 관찰자에서 이벤트 값을 재설정하는 방법
음.. 이것도 내가 사용했던 방법인데 또 나쁜 방법으로 명시되어 있었다..ㅎㅎㅎ
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
// 재설정하는 메서드 추가
fun navigateToDetailsHandled() {
_navigateToDetails.value = false
}
}
listViewModel.navigateToDetails.observe(this, Observer {
if (it) {
myViewModel.navigateToDetailsHandled() // 값을 다시 돌려놓기
startActivity(DetailsActivity...)
}
})
해결 방법 1에서 파생된 질문(🤨 여기) 메서드로 처리한 것과 유사하다는 생각이 든다.
- boilerplate한 접근이 됨 (이벤트마다 이런 처리를 해야 하는 메서드를 하나씩 생성해야 하기 때문에)
- Observer에서 이런 메서드를 호출하는 것을 잊기 쉬움
✅ 해결 방법 3 : SingleLiveEvent
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()
val navigateToDetails : LiveData<Any>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.call()
}
}
myViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
MutableLiveData를 확장해서 만든 SingleLiveEvent는 데이터를 1번만 발행하는 것을 보장한다.
userclicksOnButton() 메서드에서 직접 LiveData의 value를 변경하는 것이 아니라 call()을 호출하여 데이터를 변경한다.
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);
}
@MainThread
public void call() {
setValue(null);
}
}
- pending 값은 false로 초기화 되어 있음
- pending 값이 true일 때만 observe 메서드 내의 if 문 내의 로직을 처리하고 false로 값을 바꿈
- setValue 메서드를 통해서만 pending 값이 true로 바뀜
- 따라서 configuration 변화가 일어나도 pending 값은 false이기 때문에 observe가 작동하지 않음
- call()에서도 setValue가 일어나기 때문에 call(), setValue()를 통해야만 데이터를 observing이 가능해짐
SingleLiveEvent의 문제점
Observer가 하나☝️로 제한된다!
→ 둘 이상의 Observer가 생긴다면 하나만 호출되며, 어떤 Observer에서 데이터가 관찰되는지 알 수가 없다.
reference >
- https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
- https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java
- https://zladnrms.tistory.com/146