Android

[Android] ViewModel에서 실행한 이벤트 기다리기

728x90

 

 

 ViewModel에서 이벤트를 처리하는 방법에는 여러 가지가 있습니다. (참고 : MVVM의 ViewModel에서 이벤트를 처리하는 방법 6가지)

 이벤트에는 토스트 띄우기, 다이얼로그 보여주기 등 여러 가지 종류가 있는데요, 그중에는 사용자의 액션이 포함된 이벤트들도 있습니다. 이번 글에서는 ViewModel에서 실행한 이벤트를 기다리는 방법에 대해 알아보겠습니다.

 

 

 

 예를 들어, 아래와 같은 동작을 수행하려고 합니다.

  1. 사용자가 로그아웃을 하려고 함.
  2. 로그아웃 전에 다이얼로그로 "로그아웃을 하시겠습니까?"를 보여줌.
  3. 사용자가 OK 버튼을 누르면 로그아웃 처리 후 토스트로 "로그아웃이 되었습니다."를 보여줌.

 여기서의 이벤트는 '다이얼로그를 띄운다.', '토스트를 보여준다.' 2가지인데요, 이 중 다이얼로그를 띄운 후 사용자가 OK 버튼을 누르는 액션이 포함되어 있습니다.

 

 

 위의 예시를 구현할 때 일반적으로는 아래와 같이 작성하게 될겁니다.

  1. ViewModel에서 Dialog Event 발생
  2. Activity에서 Dialog Event를 캐치해서 다이얼로그를 띄움.
  3. 사용자가 OK 버튼을 누르면 다이얼로그의 콜백 함수에서 ViewModel의 signOut 함수 호출
  4. 로그아웃 처리 후 Toast Event 발생
  5. Activity에서 Toast Event를 캐치해서 토스트를 띄움.

너무 복잡합니다. 뷰모델과 액티비티 사이에 왔다 갔다 하는게 번거롭습니다. 하나의 함수로 이러한 동작들을 다 처리하고 싶습니다.

 

fun onClickSignOut() {
    val isOk = awaitDialogEvent()

    if (isOk) {
        doSignOut()
        
        showToast()
    }
}

 이런 식으로 코드를 짤 수 있으면 뷰모델과 액티비티 사이를 왔다 갔다 하지 않아도 되고 가독성도 좋아지게 됩니다.

 

 

 

 이러한 코드는 코루틴을 사용하면 간단하게 구현하실 수 있습니다. 코루틴은 함수를 중간에 빠져나왔다가, 다른 함수에 진입하고, 다시 원점으로 돌아와 멈춘 곳부터 다시 실행할 수 있게 해 줍니다. (참고 : 코틀린 코루틴(coroutine) 개념 익히기)

 이러한 특성을 활용해서 아래와 같은 플로우로 이벤트를 기다릴 수 있습니다.

  1. ViewModel에서 suspend function으로 이벤트 호출 후 응답을 기다린다.
  2. 사용자가 액션을 함.
  3. 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

 

728x90