ANDROID/Android 개발 이슈 & 해결

[Android] LiveData를 1번만 관찰하는 방법

주 녕 2022. 4. 6. 21:32
반응형

이번에 해결할 이슈는 LiveData의 value를 1번만 observing하는 것이다.

하고자 했던 것은 특정 기준을 달성하면 서버에서 보상에 대한 데이터를 전송해주고, 그 데이터에 대한 다이얼로그를 띄워야 했다.

하지만 LiveData로 보상 데이터를 observing하는 상태에서 해당 데이터를 다이얼로그로 띄우니

다이얼로그를 닫아도(dismiss) 계속해서 다이얼로그가 발생했다😶‍🌫️

 

 

LiveData를 사용한 이유?

 

[Android/Jetpack] AAC - LiveData

LiveData 식별 가능한(Observable) 데이터 홀더 클래스 LiveData는 Activity, Fragment, Service 등 다른 앱 구성요소의 수명 주기를 고려(Lifecycle-aware)한다. 수명 주기 인식을 통해 LiveData는 활성(active)..

junyoung-developer.tistory.com

 

LiveData 개요  |  Android 개발자  |  Android Developers

LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.

developer.android.com

우선 해결 방법 설명에 앞서, LiveData를 사용한 이유에 대해서 알아봐야 한다.

View가 ViewModel과 통신하는 편리한 방법은 Observable이 가능한 LiveData를 이용하는 것이다

→ View는 LiveData의 변경 사항을 subscribe하고 이에 반응하는 것

→ 따라서 화면에 연속적으로 표시되는 데이터에 적합하다

 

BUT snackbar, toast, navigation, dialog는 데이터를 1번만 사용해야 한다.

→ 스낵바나 토스트, 다이얼로그는 1번만 띄워지는 팝업이기 때문

→ 하지만 LiveData를 observing해서 그 값을 팝업에 사용하면 팝업이 연속적으로 표시되게 된다.

 

 

LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)

2021 Update: This guidance is deprecated in favor of the official guidelines.

medium.com

위의 블로그에서 방법을 찾을 수 있었다.

 

❌ 해결 방법 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가지 문제점이 있다고 한다.

  1. LiveData가 값을 가지고는 있지만 수신한 모든 값을 내보낸다는 보장이 없다
  2. 무엇보다.. 코드 자체가 이해하기 어렵고 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에서 파생된 질문(🤨 여기) 메서드로 처리한 것과 유사하다는 생각이 든다.

  1. boilerplate한 접근이 됨 (이벤트마다 이런 처리를 해야 하는 메서드를 하나씩 생성해야 하기 때문에)
  2. 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 >

반응형