본문 바로가기

코딩/안드로이드

Android Kotlin MVVM패턴으로 간단한 검색 앱 만들기 - 5. SingleLiveEvent, SnackbarMessage

S4장에서 계속 사용하던 LiveData의 문제점이 뭘까요?


예를들어 LiveData의 Observing을 이용해서 startActivity()를 한다고 생각해봅시다.


우리는 그럼 뷰모델에선


startActivityLiveData.value = SOME_DATA 와 같이 setValue를 하고,


액티비티에선


viewModel.startActivityLiveData.observe(this, Observe{

startActivity(어쩌구 저쩌구 클래스::class.java)

})


와 같이 실행을 하겠죠.



그렇다면 여기서 문제점이 생깁니다.


만약 startActivityLiveData를 observe하고 있는 액티비티가 회전을 하게되면?


그럼 액티비티는 startActivityLiveData가 이전에 가지고 있던 value인 SOME_DATA를 다시 observe하게 되고, 


그럼 startActivity를 통해 새로운 액티비티가 또 실행이 되겠죠.



이처럼 LiveData는 "단 한번의 이벤트"를 Observing하려고 하기에는 문제점이 약간 있습니다.



이를 고치기 위해 만든것이 SingleLiveEvent입니다.



조금 더 자세한 내용은


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


을 참고해보세요(영문).




SingleLiveEvent의 원본 코드는


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


여기에 있습니다.




우리는 이 SingleLiveEvent를 이용하여 액티비티를 실행하거나, 스낵바를 보여주는 등의 "단 한번의 이벤트"를 쉽게 발행할 수 있죠.


코드를 읽어보시면, 원리는 간단합니다.


compareAndSet과 setValue의 true로 바꿔주는 작업을 통해, setValue를 한 번 하면 observer의 코드도 단 한번만 수행해줍니다.


이렇게 setValue를 한 번 했다면 observer는 두 번할 수 없게 함으로서, '단 한번의 이벤트에 대한 Observing'을 구현합니다.





그렇다면 SnackbarMessage란 무엇일까요?



https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SnackbarMessage.java


SnackbarMessage의 코드입니다.


얜 간단하게 SingleLiveEvent를 상속하여 R.string의 id를 이용해서 snackbarmessage이벤트를 발생하는 객체입니다.




그런데 둘다 자바 코드로 되어있습니다.



간단하게 코틀린 코드로 고쳐본다면,



SingleLiveEvent


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.annotation.MainThread
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean

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

private val mPending = 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(t: T?) {
mPending.set(true)
super.setValue(t)
}

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

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




SnackbarMessage



import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer


class SnackbarMessage: SingleLiveEvent<Int>() {
fun observe(owner: LifecycleOwner, observer: (Int) -> Unit) {
super.observe(owner, Observer {
it?.run{
observer(it)
}
})
}
}

class SnackbarMessageString: SingleLiveEvent<String>(){
fun observe(owner: LifecycleOwner, observer: (String) -> Unit) {
super.observe(owner, Observer {
it?.run{
observer(it)
}
})
}
}



이렇게 되겠군요.


제가 대충 코틀린으로 번역한거라.. 어디 숨은 버그가 있을수도....있습니다 ㅋㅋ


그리고 SnackbarMessageString이란걸 제가 넣어놨는데, 저는 String의 리소스 뿐만 아니라 그냥 생 String으로도 스낵바를 가끔 쓰기 때문에 제가 하나 만들었습니다.


SnackbarMessage를 코틀린으로 짜면서, 인터페이스는 필요없어 보여서 날렸는데, 그러다보니 코드가 반이 되어버렸네요. 람다 만세





실제로 어떻게 쓰는지 봅시다.



뷰모델은 이렇게

class MainViewModel(private val model: DataModel) : BaseKotlinViewModel() {

private val TAG = "MainViewModel"

private val _startSubActivityEvent = SingleLiveEvent<Any>()
val startSubActivityEvent:LiveData<Any>
get() = _startSubActivityEvent

fun doSomething(){
.
.
.
_startSubActivityEvent.call()
}
}




액티비티는 이렇게


class MainActivity: BaseKotlinActivity<ActivityMainBinding, MainViewModel>(){
.
.
.
override fun initDataBinding(){
viewModel.startSubActivityEvent.observe(this, Observe{
startActivity(Intent(applicationContext, SubActivity::class.java))
})
}

override fun initAfterBinding(){
do_something_button.setOnClickListener{
viewModel.doSomething()
}
}
}



구현을 합시다. (지금 컴파일을 못해서 약간 틀렸을 수도 있습니다. 오류 지적 해주시면 감사하겠습니다.)


이렇게하면, do_somthing_button을 눌리면 viewModel의 doSomething()이 실행되고, startSubActivityEvent가 call()을 통해 액티비티가 바인딩하고 있는 로직을 수행합니다.


아주 간단하죠?



SnackbarMessage는 더 간단합니다.


SingleLiveEvnet처럼 observing하는것과 같은데, Generic으로 Any를 주는 것이 아니라 Int 나 String을 주는것입니다.



저의 BaseKotlinActivity를 보시면


private fun snackbarObserving() {
viewModel.observeSnackbarMessage(this) {
Snackbar.make(findViewById(android.R.id.content), it, Snackbar.LENGTH_LONG).show()
}
viewModel.observeSnackbarMessageStr(this){
Snackbar.make(findViewById(android.R.id.content), it, Snackbar.LENGTH_LONG).show()
}
}



와 같은 코드를 보실 수 있고, BaseKotlinViewModel을 보시면


fun observeSnackbarMessage(lifeCycleOwner: LifecycleOwner, ob:(Int) -> Unit){
snackbarMessage.observe(lifeCycleOwner, ob)
}
fun observeSnackbarMessageStr(lifeCycleOwner: LifecycleOwner, ob:(String) -> Unit) {
snackbarMessageString.observe(lifeCycleOwner, ob)
}


와 같은 코드를 보실 수 있습니다.



보시면, BaseKotlinActivity에서는 snackbarObserving()을 하면서 viewModel.observeSnackbarMessage에 람다를 넣어서 호출하고 있고,


BaseKotlinViewModel에서는 snackbarMessage.observe를 통해 SnackbarMessage를 Observing하고 있습니다.



또한 BaseKotlinViewModel의 showSnackbar()는


fun showSnackbar(stringResourceId:Int) {
snackbarMessage.value = stringResourceId
}
fun showSnackbar(str:String){
snackbarMessageString.value = str
}


이렇게 되어있습니다.


이를 통해 BaseKotlinViewModel을 상속하는 ViewModel은 간단하게 showSnackbar()를 호출함으로써 snackbarMessage에 setValue를 수행해주고 있으며,


아까 위에서 본것처럼 BaseKotlinActivity에선 이러한것을 observing을 통해 감지하여 주어진 람다를 수행합니다.


그리고 람다에는 Snackbar.show()가 들어가 있지요.


결국 스낵바가 보여지게 됩니다.



이 구현을 위해서 BaseKotlinActivity가 BaseKotlinViewModel의 참조를 가지고 있어야 했고, 


그럼에도 불구하고 BaseKotlinViewModel를 상속하는 객체 또한 BaseKotlinActivity를 상속하는 객체가 참조를 가지고 있었어야 했습니다.


따라서 BaseKotlinActivity를 상속할 때 두가지의 제네릭을 넣게 하여 이를 구현하였습니다.




BaseKotlinActivity와 BaseKotlinViewModel은 


https://github.com/5seunghoon/Kotlin-MVVM-Sample/tree/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/base


여기서 확인하실 수 있습니다.






여기까지 읽어주셔서 감사합니다.


이로써 제가 만든 간단한 샘플 코드의 모든 설명이 끝났습니다.



솔직히 BaseKotlinActivity와 BaseKotlinViewModel을 제가 나름 열심히 만들었지만 여전히 조잡하다고 생각합니다.


조금 더 보완이 되어서 발전하게 되면 새롭게 글을 써보도록 하겠습니다.