ViewModel에서 이벤트를 처리하는 방법에는 여러 가지가 있습니다. (참고 : MVVM의 ViewModel에서 이벤트를 처리하는 방법 6가지)
이벤트에는 토스트 띄우기, 다이얼로그 보여주기 등 여러 가지 종류가 있는데요, 그중에는 사용자의 액션이 포함된 이벤트들도 있습니다. 이번 글에서는 ViewModel에서 실행한 이벤트를 기다리는 방법에 대해 알아보겠습니다.
예를 들어, 아래와 같은 동작을 수행하려고 합니다.
- 사용자가 로그아웃을 하려고 함.
- 로그아웃 전에 다이얼로그로 "로그아웃을 하시겠습니까?"를 보여줌.
- 사용자가 OK 버튼을 누르면 로그아웃 처리 후 토스트로 "로그아웃이 되었습니다."를 보여줌.
여기서의 이벤트는 '다이얼로그를 띄운다.', '토스트를 보여준다.' 2가지인데요, 이 중 다이얼로그를 띄운 후 사용자가 OK 버튼을 누르는 액션이 포함되어 있습니다.
위의 예시를 구현할 때 일반적으로는 아래와 같이 작성하게 될겁니다.
- ViewModel에서 Dialog Event 발생
- Activity에서 Dialog Event를 캐치해서 다이얼로그를 띄움.
- 사용자가 OK 버튼을 누르면 다이얼로그의 콜백 함수에서 ViewModel의 signOut 함수 호출
- 로그아웃 처리 후 Toast Event 발생
- Activity에서 Toast Event를 캐치해서 토스트를 띄움.
너무 복잡합니다. 뷰모델과 액티비티 사이에 왔다 갔다 하는게 번거롭습니다. 하나의 함수로 이러한 동작들을 다 처리하고 싶습니다.
fun onClickSignOut() {
val isOk = awaitDialogEvent()
if (isOk) {
doSignOut()
showToast()
}
}
이런 식으로 코드를 짤 수 있으면 뷰모델과 액티비티 사이를 왔다 갔다 하지 않아도 되고 가독성도 좋아지게 됩니다.
이러한 코드는 코루틴을 사용하면 간단하게 구현하실 수 있습니다. 코루틴은 함수를 중간에 빠져나왔다가, 다른 함수에 진입하고, 다시 원점으로 돌아와 멈춘 곳부터 다시 실행할 수 있게 해 줍니다. (참고 : 코틀린 코루틴(coroutine) 개념 익히기)
이러한 특성을 활용해서 아래와 같은 플로우로 이벤트를 기다릴 수 있습니다.
- ViewModel에서 suspend function으로 이벤트 호출 후 응답을 기다린다.
- 사용자가 액션을 함.
- ViewModel의 기다리고 있던 곳에서 다시 시작해 다음 스텝을 이어 나간다.
이런 개념을 바탕으로 예시 코드를 작성해보겠습니다.
이번 글에서 이벤트 처리에 사용되는 방법은 SharedFlow + Sealed class를 사용합니다.
interface EventDelegator<T> {
suspend fun result(): T
fun tryEmit(value: T): Boolean
fun cancel(): Boolean
}
class DelegatedEvent<T : Any> : EventDelegator<T> {
private val flow = MutableSharedFlow<T?>(extraBufferCapacity = 1)
override suspend fun result(): T = flow.first() ?: throw CancellationException()
override fun tryEmit(value: T): Boolean = flow.tryEmit(value)
override fun cancel(): Boolean = flow.tryEmit(null)
}
먼저 이벤트를 기다리기 위해선 별도의 처리가 필요합니다. EventDelegator라는 인터페이스를 정의하고 구현체에서 이벤트를 기다릴 수 있게끔 해줍니다. 사용자의 액션은 tryEmit 함수를 통해 전달되고 전달된 값은 ViewModel에서 result 함수를 통해 기다리고 있다가 전달되게 됩니다.
sealed class Event
class DialogEvent(
val message: String
) : Event(), EventDelegator<Boolean> by DelegatedEvent()
class ToastEvent(
val message: String
) : Event()
위에서 정의해준 EventDelegator를 사용자의 액션이 필요한 Event가 구현하게끔 설정해줍니다. Delegate 패턴을 활용하여 간편히 사용할 수 있게끔 해줍니다.
class MainViewModel @Inject constructor() : ViewModel() {
private val _event = MutableSharedFlow<Event>()
val event = _event.asSharedFlow()
suspend fun emitEvent(event: Event) {
_event.emit(event)
}
suspend fun <T> awaitEvent(event: EventDelegator<T>): T {
if (event is Event) {
emitEvent(event)
}
return withContext(coroutineContext) {
event.result()
}
}
}
ViewModel에는 emitEvent 함수와 awaitEvent 함수 2개를 만들어줍니다. awaitEvent 함수로 이벤트를 기다릴 수 있습니다.
override fun onCreate() {
viewModel.event
.onEach { consumeEvent(this, it) }
.launchIn(lifecycleScope)
}
private fun consumeEvent(context: Context, event: Event) {
when (event) {
is ToastEvent -> {
...
}
is DialogEvent -> {
AlertDialog.Builder(context)
.setMessage(event.message)
.setPositiveButton("예") {
event.tryEmit(true)
}
.setNegativeButton("아니오") {
event.tryEmit(false)
}
.show()
}
}
}
Activity에서 정의한 이벤트들을 처리해줄 코드를 추가해줍니다. 사용자가 액션을 하면 콜백 함수를 통해 이벤트에 전달되고, 전달된 이벤트는 뷰모델로 이동됩니다.
fun onClickSignOut() {
viewModelScope.launch {
val isOk = awaitEvent(
DialogEvent(message = "로그아웃 하시겠습니까?")
)
if (isOk) {
doSignOut()
emitEvent(
ToastEvent(message = "로그아웃이 되었습니다.")
)
}
}
}
앞서 설명드렸던 방법을 사용하면 위와 같이 가독성 높은 코드 작성이 가능해집니다.
실제로 저는 프로젝트에서 다양한 이벤트를 위와 같은 방식을 사용해서 처리해주고 있습니다. 이를 잘 활용하면 특정 화면으로 이동 후 돌아왔을 때 처리, 시스템 설정에서 알림 켜고 난 뒤의 처리 등등 복잡한 액션에 대해서도 처리가 가능합니다. 이번 글에 오류가 있거나 이벤트 처리 관련해서 더 좋은 방법 있으시면 댓글 남겨주세요. 감사합니다. :)
예제 소스
https://github.com/tkdgusl94/blog-source/tree/master/Suspend-Event
GitHub - tkdgusl94/blog-source: https://leveloper.tistory.com/ 에서 제공하는 예제 source
https://leveloper.tistory.com/ 에서 제공하는 예제 source. Contribute to tkdgusl94/blog-source development by creating an account on GitHub.
github.com
'Android' 카테고리의 다른 글
[Android] Clean Architecture에서 Paging 라이브러리 사용하기 - 도메인 계층의 의존성 문제 (5) | 2022.06.01 |
---|---|
[Android] MVVM 패턴과 AAC에서의 ViewModel (8) | 2021.10.06 |
[Android] Gson을 이용한 Room에 다양한 타입의 데이터 저장하기 (0) | 2021.09.24 |
[Android] 스켈레톤 로딩 화면 구현하기 - Facebook shimmer library (0) | 2021.09.13 |
[Android] viewModelScope.launch() 간단하게 바꿔보기 (0) | 2021.09.11 |