MVVM 구조에서 ViewModel 이벤트를 처리하는방법 메모

MSU·2024년 6월 13일

Android

목록 보기
8/36

※ 헤이딜러 안드로이드 팀의 글을 참조했다.
https://medium.com/prnd/mvvm%EC%9D%98-viewmodel%EC%97%90%EC%84%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-6%EA%B0%80%EC%A7%80-31bb183a88ce

Observe LiveData

LiveData를 observe해서 값이 바뀔 때 이벤트를 처리하는 방법

A activity에서 observe로 토스트를 띄우는 이벤트를 처리했는데 B activity로 갔다 돌아오면 A activity로 다시 돌아왔을 때 이전에 실행된 토스트가 한번 더 띄워지는 문제가 발생한다.
왜냐하면 LiveData를 observe 하고 있는 observer는 inactive상태에서 active가 될 때 항상 마지막 값을 emit한기 때문이다.
따라서 한번 발생한 이벤트가 다시 발생하지 않도록 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
}

StateFlow, SharedFlow

ViewModel 에서 LiveData를 사용하게되면 상단에 import android.xxx 와 같이 안드로이드 프레임 워크에 종속될 수 밖에 없는데 이런 종속성을 해결하기 위해 StateFlow, SharedFlow로 대체할 수 있다.

LiveData -> StateFlow
Event Wrapped LiveData -> SharedFlow
observe -> collect

SharedFlow + Sealed class

처리해야할 이벤트가 여러개일 때 이벤트 만큼 SharedFlow를 만들수 있지만 그에 따라 각각의 SharedFlow를 collect해주어야하는 번거로움이 있다.

따라서 이벤트를 전파하는 하나의 flow만 만들고 여러개의 이벤트를 Seald class 형태로 만들어서 분기하여 처리해줄 수 있다.

// ViewModel

@HiltViewModel
class Step4ViewModel @Inject constructor() : ViewModel() {

    private val _eventFlow = MutableSharedFlow<Event>()
    val eventFlow = _eventFlow.asSharedFlow()

    fun showToast() {
        event(Event.ShowToast("토스트"))
    }

    fun aaa() {
        event(Event.Aaa("aaa"))
    }

    fun bbb() {
        event(Event.Bbb(36))
    }

    private fun event(event: Event) {
        viewModelScope.launch {
            _eventFlow.emit(event)
        }
    }

    sealed class Event {
        data class ShowToast(val text: String) : Event()
        data class Aaa(val value: String) : Event()
        data class Bbb(val value: Int) : Event()
    }
}
// UI에서는 한개의 eventFlow만 collect

lifecycleScope.launch {
    viewModel.eventFlow.collect { event -> handleEvent(event) }
}
...
private fun handleEvent(event: Event) = when (event) {
    is Event.ShowToast -> // TODO
    is Event.Aaa -> // TODO
    is Event.Bbb -> // TODO
}

+ repeatOnLifecycle

Lifecycle에서 repeatOnLifecycle이라는 함수가 추가되었는데 lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 이상 버전부터 사용 가능하다

따라서 생명주기에 맞추어 OnStart나 OnStop에서 코드를 작성해줄 필요가 없게 된다.

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Create a new coroutine from the lifecycleScope
        // since repeatOnLifecycle is a suspend function
        lifecycleScope.launch {
            // Suspend the coroutine until the lifecycle is DESTROYED.
            // repeatOnLifecycle launches the block in a new coroutine every time the 
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Safely collect from locations when the lifecycle is STARTED
                // and stop collecting when the lifecycle is STOPPED
                someLocationProvider.locations.collect {
                    // New location! Update the map
                }
            }
            // Note: at this point, the lifecycle is DESTROYED!
        }
    }
}

+ 이벤트 캐싱

이벤트가 발생하여 처리하기 전에 홈화면을 눌러 앱이 백그라운드로 내려가게 되면 해당 이벤트는 유실되어 다시 홈화면에서 앱으로 돌아올 때 이벤트가 발생했던 것을 알 수 없게 된다.
따라서 이벤트가 발생할 때 이를 캐시하고 있다가 이벤트의 consume 여부에 따라서 새로운 observer가있을 때 이벤트를 발생시킬 지 여부를 결정해주면 된다.

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)
}
profile
안드로이드공부

0개의 댓글