본문 바로가기
Android 일지/리팩토링

4. EventFlow 도입

by 쎄오SseO 2023. 5. 26.

(RunWithMe 리팩토링) 4. EventFlow 도입

 

 

안녕하세요

RunWithMe 리팩토링 프로젝트에서 팀장과 안드로이드 개발을 맡은 서경원입니다.

RunWithMe 프로젝트를 리팩토링하면서 공부한 내용에 대해서 설명해보려합니다.

이해가 안되는 내용이나 제가 잘못 적은 부분이 있다면 꼭 댓글 남겨주세요.

 

 

 

이 글은 Ted Park님의 블로그를 참고하여 작성했습니다.

MVVM의 ViewModel에서 이벤트를 처리하는 방법 6가지

 

MVVM의 ViewModel에서 이벤트를 처리하는 방법 6가지

지금 개발하시는 코드에서 ViewModel의 이벤트 처리를 어떻게 하고 계신가요? 헤이딜러에서 LiveData -> SingleLiveData -> SharedFlow -> EventFlow로 이벤트 처리 방법을 변화 하기까지 과정을 소개합니다…

medium.com

 

면접에서 MVVM 패턴에서의 Event 처리에 대해 문제점이 생길 수 있다는 것을 배우고 난 뒤, 어떤 문제점이 있는지 찾아보게 되었고 Ted Park님의 글을 보고 많이 배울 수 있었습니다.

 

 

 

SingleLiveEvent

기존 RunWithMe 코드에서는 SingleLiveEvent를 통하여 Event를 처리하고 있었습니다. LiveData나 Flow를 사용해서 Event를 처리하지 않고 왜 SingleLiveEvent를 사용했는 지에 대한 의문점을 가질 수 있습니다. 그 이유는 아래와 같은 문제점이 있기 때문입니다.

 

 

아래 예시는 다음과 같이 동작합니다.

  1. 버튼을 누르면 ViewModel의 LiveData에 setValue를 한다.
  2. Activity에서는 LiveData를 observe하고 observe가 실행될 때 토스트를 띄운다.
  3. 그 이후 화면 회전을 동작해본다.

 

 

원래 예측한대로라면 화면을 회전하고 난 이후에는 버튼을 클릭하지 않았기 때문에 토스트가 뜨지 않아야 합니다.

 

하지만, 화면 회전의 경우에는 액티비티가 재실행 되면서 옵저버가 다시 등록되고 값이 전달되는 과정을 거치게 됩니다.

 

LiveData에 전달된 value가 없다면 재실행되어도 활성화되지 않겠지만, LiveData는 ViewModel에 존재하고 액티비티가 회전하는 과정에서는 VIewModel이 destroy되지 않아 LiveData에도 그대로 value가 존재하기 때문입니다.

 

로그를 살펴보면 액티비티가 재실행되는 과정에서 onStart 이후 observe가 실행되고 그 이후 onResume이 실행됩니다.

 

 

이러한 문제 때문에 event 처리를 위해 SingleLiveEvent를 사용합니다.

 

import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean

class SingleLiveEvent<T> : MutableLiveData<T>() {

    companion object {
        private const val TAG = "SingleLiveEvent"
    }

    val mPending: AtomicBoolean = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        if (hasActiveObservers()) {
            Log.w(TAG,"Multiple observers registered but only one will be notified of changes.")
        }

        // Observe the internal MutableLiveData
        super.observe(owner, Observer { t ->
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(@Nullable t: T?) {
        mPending.set(true)
        super.setValue(t)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }

}

 

SingleLiveEvent는 mPending이라는 AtomicBoolean을 통해 observe하게 되어있습니다.

mPending을 비교하여 true면 false로 변경하고 observer.onChanged 를 동작하게 합니다.

이를 통해 한 번만 observer가 호출될 수 있도록 합니다.

 

 

 

하지만,

Clean Architecture 관점에서 보면, 플랫폼 종속성은 View로 몰아두고 Presenter는 종속성이 없는 것이 좋습니다. ViewModel은 Presenter에 속하기 때문에 안드로이드 종속성을 없애주는 것이 좋고, LiveData는 안드로이드 플랫폼에 속하기 때문에 Flow로 변경해주는 것이 좋다고 합니다.

 

플랫폼 종속성을 없애고, 데이터 홀더로 Flow를 사용하면서 Event처리만 LiveData를 사용했기 때문에 통일성을 맞추기 위해서라도 Event 처리를 Flow로 변환해주고자 했습니다.

 

 

 

Flow

Flow의 emit이나 collect는 ifecycleScope를 통해 주로 동작하게 합니다. 이 Scope는 lifecycleOwner를 가진 Scope로 부모의 생명주기에 따라 동작하기 때문에 부모 뷰가 destroy되면 동작을 멈춥니다.

 

하지만, activity가 백그라운드로 간다면? onStop인 상태로 collect가 계속 동작하고 있을 것입니다.

한 마디로, 백그라운드에 있을 때는 collect를 할 필요가 없는 것입니다.

 

이를 해결하기 위해, repeatOnLifecycle을 사용합니다.

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        // collect
    }
}

위 코드 처럼 STARTED 속성을 주고 사용하게 된다면 부모 뷰의 생명 주기가 onStart ~ onStop 사이에서만 동작하게 됩니다. 이를 통해 백그라운드에서 collect가 동작하지 않게 할 수 있습니다.

 

 

 

EventFlow

하지만 SharedFlow, StateFlow를 사용하게 된다면 또 문제가 생깁니다. 두 flow는 hot 채널입니다. hot 채널은 collect로 구독하고 있는 상태가 아니더라도 emit을 통해 값을 계속 방출합니다.

 

emit을 통해 방출하기 전에 화면이 백그라운드로 가버린다면?

 

emit을 통해 방출한 값은 유실되어 event를 collect하지 못하게 됩니다.

이를 해결하기 위해 만든 것이 EventFlow입니다. EventFlow 내에는 consumed 변수가 존재합니다. consumed를 통해 Event를 캐시하고 있게 하는 것으로, consumed가 false로 존재한다면 아직 collect하지 않았다는 뜻입니다.

또한, SharedFlow의 replay의 default를 3으로 설정함으로써 이전 값을 3개까지 다시 보낼 수 있게 합니다.

consumed를 통해 화면이 백그라운드로 내려가는 경우에 유실되는 event를 처리할 수 있게 해줍니다.

 

interface EventFlow<out T> : Flow<T> {

    companion object {

        const val DEFAULT_REPLAY: Int = 3
    }
}

interface MutableEventFlow<T> : EventFlow<T>, FlowCollector<T>

@Suppress("FunctionName")
fun <T> MutableEventFlow(
    replay: Int = EventFlow.DEFAULT_REPLAY
): MutableEventFlow<T> = EventFlowImpl(replay)

fun <T> MutableEventFlow<T>.asEventFlow(): EventFlow<T> = ReadOnlyEventFlow(this)

private class ReadOnlyEventFlow<T>(flow: EventFlow<T>) : EventFlow<T> by flow

private class EventFlowImpl<T>(
    replay: Int
) : MutableEventFlow<T> {

    private val flow: MutableSharedFlow<EventFlowSlot<T>> = MutableSharedFlow(replay = replay)

    @InternalCoroutinesApi
    override suspend fun collect(collector: FlowCollector<T>) = flow
        .collect { slot ->
            if (!slot.markConsumed()) {
                collector.emit(slot.value)
            }
        }

    override suspend fun emit(value: T) {
        flow.emit(EventFlowSlot(value))
    }
}

private class EventFlowSlot<T>(val value: T) {

    private val consumed: AtomicBoolean = AtomicBoolean(false)

    fun markConsumed(): Boolean = consumed.getAndSet(true)
}

 

 

아래는 블로그에 올라온 Ted Park님의 github 코드를 참고하여 적용한 코드입니다.

회원 가입 api를 실행한 후 Event 처리에 대한 코드입니다.

 

// Util

fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) {
    lifecycleScope.launch {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block)
    }
}
// JoinViewModel

@HiltViewModel
class JoinViewModel @Inject constructor(
    private val joinUseCase: JoinUseCase
) : ViewModel() {

    private val _joinEventFlow = MutableEventFlow<Event>()
    val joinEventFlow get() = _joinEventFlow.asEventFlow()

    fun join(user: User) {
        viewModelScope.launch(Dispatchers.IO) {
            joinUseCase(user).collectLatest {
                when (it) {
                    is ResultType.Success -> {
                        _joinEventFlow.emit(Event.Success("회원가입 완료"))
                    }

                    is ResultType.Fail -> {
                        _joinEventFlow.emit(Event.Fail(it.data.message))
                    }

                    is ResultType.Error -> {
                        Log.d("joinError", "${it.exception.message} ")
                    }
                    else -> {

                    }
                }
            }
        }

    }

        // sealed를 사용한 Event 분류
    sealed class Event {
        data class Success(val message: String) : Event()
        data class Fail(val message: String) : Event()

    }

}
// JoinActivity

@AndroidEntryPoint
class JoinActivity : BaseActivity<ActivityJoinBinding>(R.layout.activity_join) {

    private val joinViewModel by viewModels<JoinViewModel>()


    ...

    private fun initViewModelCallBack() {

        repeatOnStarted {
            joinViewModel.joinEventFlow.collectLatest { event ->
                handleEvent(event)
            }
        }

    }

    private fun handleEvent(event: Event) {
        when (event) {
            is Event.Success -> {
                showToast(event.message)
                finish()
            }
            is Event.Fail -> {
                showToast(event.message)
            }
        }
    }

}