💬 Single Event 처리
ViewModel의 LiveData를 Observe하고 있으며 값이 True로 변경되면 Toast 메시지를 출력합니다.
그러나, 만약, 디바이스 화면을 회전(Configuration)하였더니 Toast가 한 번 더 호출되는 이슈가 발생합니다
이는 Configuration이 발생하면, 기존 Activity가 Destroy되고 화면이 새롭게 그려지면서 onCreate() 콜백 메서드가 다시금 호출되기 때문입니다.
ViewModel은 일반적으로 Activity보다 생명주기가 길기 때문에, Activity가 파괴된다고 하더라도 ViewModel 내부의 필드는 그대로 유지됩니다.
그렇기 때문에 LiveData가 감싸고 있는 데이터는 True 상태를 유지하고 있으며, Activity에서는 화면이 다시 그려지면서 LiveData를 다시 Observe()하여 Observer의 상태가 inactive -> active로 변경되면서 Toast를 출력하게 되는 것입니다.
이에 대해 Google에서 제안하는 해결책이 있습니다.
💡 Event Wrapper
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): T = content
}
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
Event클래스는 Generic 타입의 content를 입력받습니다.
그리고 만약 해당 content를 getContentIfNotHandled() 라는 메서드를 통해 접근할 수 있습니다.
여기서 핵심은 hasBeenHandled라는 필드를 두어 만약 한 번 접근한 적이 있다면 null을 반환하여 두 번 이상 다루지 않도록 구현할 수 있습니다.
반면, EventObserver클래스는 Observer를 상속받고 있습니다.
그리고 값이 변경되었을 때 getContentIfNotHandled() 메서드를 통해 접근하는데, 여기서 위에 설명한 바와 같이 이미 한 번 접근한 적이 있다면 null이 반환되므로 event가 실행되지 않도록 구현되어 있습니다.
이 2가지 클래스를 적절히 활용하여 불필요하게 두 번 이상 event가 호출되는 상황을 방지할 수 있습니다.
아래 예시 코드를 통해 활용을 살펴보겠습니다.
class MainViewModel : ViewModel() {
private val _error = MutableLiveData<Event<Boolean>>()
val error: LiveData<Event<Boolean>> = _error
fun invokeError() {
_error.value = Event(true)
}
}
backingField를 사용하는 등 기존에 ViewModel에서 LiveData를 사용하는 방법을 동일합니다.
하지만, 조금 다른 점은 위에서 제공하는 Event클래스로 Boolean을 한 번 더 감싸고 있다는 점입니다.
만약 invokeError() 메서드를 호출하면 Event(true)로 값이 업데이트되면서 Observer에 이벤트 변경 감지가 일어날 것입니다.
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val errorButton = findViewById<Button>(R.id.error_invoke_button)
errorButton.setOnClickListener {
mainViewModel.invokeError()
}
mainViewModel.error.observe(this, EventObserver { isOccurredError ->
if (isOccurredError) {
Toast.makeText(this, "에러가 발생했습니다.", Toast.LENGTH_SHORT).show()
}
})
}
}
반면, MainActivity는 ViewModel의 error를 관찰하고 있습니다.
이 또한, 기존에 사용하던 Observer 대신에 새롭게 만들어준 EventObserver를 사용하고 있다는 점 외에는 달라진 부분이 없습니다.
하지만 이런 간단한 방법만으로 불필요하게 변경이 두 번 감지되는 상황을 방지할 수 있습니다.
⭐ 결론
- ViewModel + Event Wrapepr 패턴을 활용하여 변경이 한 번만 감지되어야 하는 상황을 구현할 수 있다.
github : https://github.com/tmdgh1592
'Android' 카테고리의 다른 글
[Android] Listview vs RecyclerView (1) | 2023.04.20 |
---|---|
[Android] This version of the Android Support plugin for IntelliJ IDEA (or Android Studio) 오류 해결 (2) | 2022.12.23 |
[Android] Firebase Dynamic Link를 활용하여 사용자 초대링크 생성하기 (2) | 2022.02.08 |
[Android] 자바 코틀린 (Pattern, Matcher)정규식을 사용하여 패스워드 조건을 만들어보자 (0) | 2022.02.05 |
[Android][어따세워] 음성인식으로 주차장을 검색해보자! STT(Speech To Text) SpeechRecognizer (2) | 2021.12.11 |