
의역 및 오역이 있을 수 있습니다.
원본 글: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
서론
View(Activity 또는 Fragment)와 ViewModel이 데이터를 주고받기 위한 방식으로 LiveData를 주로 이용합니다. View에서 LiveData를 subscribe하여 LiveData에 변화가 일어나면 반응합니다. 이런 방식은 화면에 지속적으로 표시되는 데이터들에서는 잘 동작합니다.
하지만 Snackbar 메세지나 네비게이션 이벤트, dialog 표시 등과 같이 한번만 발생되는 이벤트들도 있습니다.
이러한 이벤트를 처리하는 여러가지 방식에대해서 알아보고 어디가 문제이며 잘못되었는지 알아보겠습니다.
❌ Bad 1: LiveData를 이용한다.
아래의 코드는 navigate의 signal을 LiveData에 직접 삽입합니다. 이런 방식은 일반적인 LiveData를 이용하는 것 처럼 쓸 수 있을 것 처럼 보이지만 몇몇 문제점이 있습니다.
코드
ViewModel
// 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
}
}
View
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
문제점
여기서 발생할 수 있는 문제는, _navigateToDetails가 계속 true로 설정되어있어 초기 화면으로 돌아갈 수 없는 상황이 일어날 수 있다는 것이다. 이 경우를 순서대로 알아보자.
- 유저가 버튼을 눌러서 Details 액티비티가 시작된다.
- 유저가 back버튼을 눌러 list 액티비티로 돌아간다.
- observer가 활성화된다.
- _navigateToDetails는 여전히 true이므로 Details 액티비티가 다시 시작된다.
(observe는 일반적으로 값의 변화가 일어날 때, 콜백이 실행되지만 비활성화상태에서 활성화 되었을 때도 실행된다.)
이를 해결하기 위해서 _navigateToDetails값을 true로 바꾼 뒤, 바로 false로 설정해서 이벤트는 트리거 하되, 값은 false로 유지하는 방법이 있을 수 있다.
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don't do this
}
하지만 LiveData는 값을 저장할 뿐, 매 저장시마다 값 변경을 알리는 것을 보장하지는 않는다. 예를들어 현재 활성화된 observer가 없을 때, 값을 변경하면, observer는 값 변경을 모를 수 밖에 없다. 그리고 여러 스레드에서 값을 변경하게 되면 race condition이 발생할 수도 있다.
❌ Better 2: LiveData를 이용하되, observer에서 값을 초기화.
코드
ViewModel
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
fun navigateToDetailsHandled() {
_navigateToDetails.value = false
}
}
View
listViewModel.navigateToDetails.observe(this, Observer {
if (it) {
myViewModel.navigateToDetailsHandled()
startActivity(DetailsActivity...)
}
})
문제점
이 방식의 문제점은 코드 재사용이 불가능 하다는 점이다. 예를들어 details, setting, main, mypage등 여러 메뉴를 navigate해야한다면, 각각 메뉴에 따른 핸들러를 따로 만들어야한다. 또한 View에서 핸들러를 호출하는것을 까먹어 에러를 일으킬 수도 있다.
✔️ OK: SingleLiveEvent이용하기
SingleLiveEvent는 한번 업데이트되는 LiveData이다.
GitHub - android/architecture-samples: A collection of samples to discuss and showcase different architectural tools and pattern
A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - GitHub - android/architecture-samples: A collection of samples to discuss and showcase...
github.com
코드
ViewModel
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()
val navigateToDetails : LiveData<Any>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.call()
}
}
View
myViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
문제점
SingleLiveEvent는 하나의 Observer가 있을 때만 사용할 수 있다. 여러 Observer가 있다면 어떤곳에서 호출 되었는지 알 수 없을 것이다.
✔️ Recommended: Event wrapper 이용하기
코드
Event
/**
* 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
}
ViewModel
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
View
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
EventWrapper를 이용하면 getContentIfNotHandled로 해당 이벤트가 handle되었는지 체크 할 수 있고, 하나의 event가 여러 Observer를 가질 수 있습니다.
'Develop > Kotlin' 카테고리의 다른 글
[Kotlin, Android Studio] 삽질 방지 오류 해결 방법 모음 (0) | 2022.03.14 |
---|---|
[Kotlin Coroutines] 코루틴 완전 정복 #1 개요, 코루틴의 특징 (0) | 2021.11.26 |
[Kotlin] 안드로이드 개발자 기술 면접 정리 (0) | 2021.11.14 |
Kotlin 공식문서 둘러보기 #1 코틀린의 장점, 기본 문법 (0) | 2021.09.06 |
[안드로이드 개발자 가이드] 앱 기본 요소 (0) | 2021.08.23 |