최근 프로젝트에서 LiveData 이용해 MVVM을 만드는 경험을 하였습니다. 프로젝트를 만들다 보니 ViewModel에서 특정 이벤트가 발생을 하였는데, 딱히 값을 넘겨줄 필요가 없는 경우가 있었습니다. 이러한 경우 null을 넣어주거나 사용하지 않는 불필요한 값을 넣어주거나 하는 방법을 사용할 수 있는데 뭔가 섞연치 않아서 찾아보다 보니 이에 대한 해답으로 구글 에서 SingleLiveEvent 란 예시를 주었습니다.

예를 들어서 ViewModel에서 startMyActivity()란 메소드 호출이 발생하면 View에서 감지하고 있다가 MyActivity::class.java를 실행해야한다고 가정했을때 LiveData를 이용해서 구현하려면 다음과 같이 구현해야 할것입니다.

class MyViewModel : BaseViewModel() {

    val eventMyActivity: MutableLiveData<Boolean> = MutableLiveData() // 타입은 상관없지만 여기선 Boolean으로 하겠습니다.

    fun startMyActivity() {
        eventMyActivity.value = true
    }
    
}

[MyViewModel.kt]

이제 View에서는 다음 LiveData를 감지하고 있어야 할것입니다.

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel = MyViewModel()

        viewModel.eventMyActivity.observe(this, {
            Intent(this, MyActivity::class.java).apply { 
                startActivity(this)
            }
        })

    }
}

[MainActivity.kt]

여기서 큰 문제가 있는데 eventMyActivityLiveData에 사용하지도 않는 불필요 값을 넣어야 합니다. 심지어 여기서 아래 두 케이스 전부 동작 합니다.


eventMyActivity.value = true
eventMyActivity.value = false

이에 구글에서는 SingleLiveEvent를 예시로 줬습니다. 우선 아래의 SingleLiveEvent.kt를 만듭니다.

import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean;


class SingleLiveEvent<T> : MutableLiveData<T>() {

    companion object {
        private const val TAG = "SingleLiveEvent"
    }

    val mPending: AtomicBoolean = AtomicBoolean(false)

    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 (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(@Nullable t: T?) {
        mPending.set(true)
        super.setValue(t)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }

}

[SingleLiveEvent.kt]

그 다음에 ViewModel에서 다음과 같이 사용할 수 있습니다.

import androidx.lifecycle.LiveData

class MyViewModel : BaseViewModel() {

    private val _eventMyActivity = SingleLiveEvent<Any>()
    val eventMyActivity: LiveData<Any>
        get() = _eventMyActivity
            
            
    fun startMyActivity() {
        _eventMyActivity.call()
    }

}

[MyViewModel.kt]

그 다음 View에서는 다음과 같이 사용할 수 있습니다.

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel = MyViewModel()

        viewModel.eventMyActivity.observe(this, {
            Intent(this, MyActivity::class.java).apply { 
                startActivity(this)
            }
        })

    }
}

[MainActivity.kt]

이렇게 하게 되면 불필요한 값을 넘겨주지 않고 call() 이란 메소드를 호출하는 것만으로도 View에서 감지하여 이벤트를 실행할 수 있습니다.

하지만 여기에 또다른 문제(?)가 있습니다. 모든 이벤트마다 매번 SingleLiveData 객체를 만들어줘야 하는 문제가 발생합니다. 여기서 위에 글에서는 Event.kt를 사용하라고 하였습니다.

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
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.kt]

저는 여기서 만든 EventBaseViewModel이란 모든 ViewModel이 상속받는 SuperClass에 다음과 같이 넣어 주었습니다.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

open class BaseViewModel : ViewModel() {

    private val _viewEvent = MutableLiveData<Event<Any>>()
    val viewEvent: LiveData<Event<Any>>
        get() = _viewEvent

    fun viewEvent(content: Any) {
        _viewEvent.value = Event(content)
    }

}

[BaseViewModel.kt]

이렇게 한 다음에 ViewModel에서 다음과 같이 사용할 수 있습니다.

class MyViewModel : BaseViewModel() {

    companion object {
        const val EVENT_START_MY_ACTIVITY = 22212
    }

    fun startMyActivity() = viewEvent(EVENT_START_MY_ACTIVITY)

}

[MyViewModel.kt]

그 다음 View에서는 이렇게 사용합니다.

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel = MyViewModel()

        viewModel.viewEvent.observe(this, {
            it.getContentIfNotHandled()?.let { event -> 
                when (event) {
                    MyViewModel.EVENT_START_MY_ACTIVITY -> {
                        Intent(this, MyActivity::class.java).apply {
                            startActivity(this)
                        }
                    }
                }
            }
        })

    }
}

[MainActivity.kt]

이렇게 하게 되면 매번 SingleLiveEvent를 만들 필요도 없고, 여러 이벤트에 대해서 한번에 처리할 수도 있습니다. 여기서 MyActivity2란 액티비티를 호출하는 이벤트를 하나 더 추가해보겠습니다.

class MyViewModel : BaseViewModel() {

    companion object {
        const val EVENT_START_MY_ACTIVITY = 22212
        const val EVENT_START_MY_ACTIVITY_2 = 22213
    }

    fun startMyActivity() = viewEvent(EVENT_START_MY_ACTIVITY)
    
    fun startMyActivity2() = viewEvent(EVENT_START_MY_ACTIVITY_2)

}

[MyViewModel.kt]

그 다음에 View에서도 MyActivity2 만 호출하는 부분을 추가합니다.

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.global.exoplayer.MyViewModel.Companion.EVENT_START_MY_ACTIVITY_2

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel = MyViewModel()

        viewModel.viewEvent.observe(this, {
            it.getContentIfNotHandled()?.let { event ->
                when (event) {
                    MyViewModel.EVENT_START_MY_ACTIVITY -> {
                        Intent(this, MyActivity::class.java).apply {
                            startActivity(this)
                        }
                    }
                    EVENT_START_MY_ACTIVITY_2 -> {
                        Intent(this, MyActivity2::class.java).apply {
                            startActivity(this)
                        }
                    }
                }
            }
        })

    }
}

[MainActivity.kt]

이렇게 하게되면 굳이 데이터를 넘기지 않아도 되는 이벤트들을 좀 더 수월하게 처리할 수 있게 됩니다. 마지막 팁으로 자주 사용되는 기능들을 BaseViewModel과 같은 SuperClass에 넣어놓고 사용하면 코드 재활용을 수월하게 할 수 있습니다.