안녕하세요. 점냥입니다:)
이번 포스팅 주제로는 SingleLiveData
입니다.
Android AAC LiveData의 사용법 중 하나로 LiveData를 아직 잘 모르신다면 링크를 먼저 읽어와 주세요!
LiveData의 변경된 Data를 오직 한 번만 관찰?
LiveData는 AAC ViewModel에서 주로 쓰이는 클래스로 뷰의 수명주기에 알맞게 데이터를 저장하는 역할을 가지고 있습니다.
ViewModel에 존재하기 때문에 LiveData들은 디바이스의 가로/ 세로 전환 등 뷰가 재생성될 때에도 데이터가 유지됩니다.
따라서 뷰는 데이터를 다시 네트워크나 내부 저장소에서 가져올 필요가 없이 그대로 이전 데이터를 다시 사용할 수 있죠.
그런데 이러한 특성을 가진 ViewModel + LiveData 패턴으로 Dialog, Toast, SnackBar를 띄우는 기능을 구현할 때 문제가 발생할 수 있습니다. 변경된 데이터가 뷰의 재생성에 따라서 여러 번 사용할 수 있기 때문에 오직 한 번만 떠야 하는 Dialog에는 맞지 않는 것입니다.
그럼에도 불구하고 해결하기 위해 여러 가지 작성했던 코드들을 살펴보고 문제점을 알아봅시다.
나쁜 방법 : LiveData<Boolean> 값을 사용
이벤트가 발생했다는 것을 Boolean 자료형을 사용하여 구현하는 방법입니다.
in ViewModel.
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
}
in View.
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
코드를 보면 LiveData에 데이터가 Boolean 데이터로 변경되고, 뷰에서 구독하며 true일 때 DetailActivity 화면으로 전환하는 코드입니다.
여기서 문제점은 true로 변경되어 DetailActivity로 전환되고 나서 사용자가 BackButton을 누르고 돌아왔을 때 문제가 됩니다.
이런 경우 뷰가 다시 재활성화되면서 LiveData의 데이터가 활성화되고 뷰에서도 LiveData를 관찰하게 됩니다. 따라서 이전에 설정한 true 값으로 인해 DetailActivity 화면으로 다시 전환되는 큰 오류가 발생하게 됩니다.
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false //이 작업을 수행하지 마십시오.
}
어떤 사용자들은 "그렇다면 true로 변경해준 뒤 false로 변경해 주면 되지 않을까요?"라며 해결책을 제시할 수도 있지만
이는 다음과 같은 문제점이 있다고 합니다.
- LiveData는 변경되는 모든 데이터를 발행하지 않을 수 있다. 따라서 true, false 모든 값을 발행하지 않고 마지막 값은 false만 발행할 수 있다.
- 코드의 가독성이 올바르지 않다. 무슨 기능을 하는 코드인지 파악하기 힘듦
나쁜 방법 : 관찰자 쪽에서 LiveData의 값을 재설정
in View.
listViewModel.navigateToDetails.observe (this ,Observer {
if (it) {
myViewModel.navigateToDetailsHandled ()
startActivity ( DetailsActivity ... )
}
})
in 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
}
}
그다음 해결책으로 관찰하는 뷰에서 false로 재설정하는 ViewModel의 함수를 호출하는 방법입니다.
이 방법의 문제점은 한 번만 데이터를 관찰해야 하는 기능 당 이러한 재설정하는 함수들이 생성되어야 하고, View에서 이러한 재설정하는 코드를 꼭 호출해야 하는 의존성이 생기는 것입니다.
올바른 방법 1. SingleLiveData
SingleLiveData는 MutableLiveData를 확장하여 만든 클래스로 오직 한 번만 데이터를 발행하는 것을 보장한다고 합니다.
in ViewModel.
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()
val navigateToDetails : LiveData<Any>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.call()
}
}
in View.
myViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
오직 데이터를 한번만 관찰할 수 있음을 보장하지만 SingleLiveData도 문제점이 하나 있다고 합니다.
관찰하는 관찰자가 오직 1명밖에 설정할 수 없다고 해요. 2명 이상의 관찰자를 설정하면 1명의 관찰자에게만 데이터 변경 알림이 가고 어느 쪽에 가는지 알 수 없다고 합니다.
올바른 방법 2. Event wrapper 클래스 사용
/**
* 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는 Data를 한번 감싸면서 오직 한 번만 데이터를 접근하게 만드는 클래스입니다.
이 방법의 장점은 여러 번 데이터를 접근할 때도, 오직 한 번만 데이터를 접근할 때도 모두 사용가능하며 후자의 경우에는
getContentIfNotHandled()
함수를 이용해 뷰에서 오직 한 번만 데이터를 접근할 수 있게 구현이 됩니다.
In 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) // 새 이벤트를 새 값으로 설정하여 이벤트 트리거
}
}
In View.
myViewModel.navigateToDetails.observe(this ,Observer {
it.getContentIfNotHandled()?.let { // 이벤트가 처리되지 않은 경우에만 진행
startActivity ( DetailsActivity ... )
}
})
참고
'Android > Common' 카테고리의 다른 글
[Android] fromHtml Bullet 속성 변경하기 (0) | 2021.07.03 |
---|---|
[Android] RecyclerView - ConcatAdapter (8) | 2021.04.04 |
[Android] MVVM 적용하기 - View와 ViewModel (2) | 2021.03.10 |
[Android] 상태바 투명으로 만드는 여러 방법에 대한 일지 (0) | 2021.03.08 |
[Android] Android Font 직접 적용 (0) | 2021.03.02 |