우찬쓰 개발블로그

Event용 LiveData 적용하기 본문

안드로이드/안드로이드 개발

Event용 LiveData 적용하기

이우찬 2021. 4. 9. 12:23
반응형

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

github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java

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를 통해 문제를 해결하는 쪽이 깔끔해 보였다.

 

그래서 위 소스를 참고하여 변형하여 만든 필자의 소스는 다음과 같다.

github.com/WoochanLee/CatHolic/blob/master/app/src/main/java/com/woody/cat/holic/framework/base/Event.kt

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가 훨씬 좋아진 것을 확인할 수 있다.

 

 

(참고문헌)

medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

반응형
Comments