안드로이드 연구소

Flow 심화2: EventFlow 본문

안드로이드 연구소/비동기

Flow 심화2: EventFlow

안드로이드 연구원 2023. 11. 13. 10:35

지난번에 게시물에서 sharedFlow와 emit을 사용해서

MVVM패턴에서 viewModel 이벤트 처리하였습니다.

이제 그 다음 단계로 넘어가보겠습니다.

 

출처: 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

 

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

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

medium.com

(사랑해요 박혁권)

 

1. SharedFlow+collect에서 여러개의 Event 처리한다면?

일반적으로 sharedFlow와 collect 기존 방식으로 이벤트를 두개, 세개 처리해야한다면

아래처럼 viewModel에서 여래개의 SharedFlow를 만들어주고

Activity나 Fragment에서 각 각의 SharedFlow데이터를 collect로 연결해주어야합니다. 

 

viewModel

private val _event1 = MutableSharedFlow<String>()
val event1 = _event1.asSharedFlow()

private val _event2 = MutableSharedFlow<Boolean>()
val event2 = _event2.asSharedFlow()

private val _event3 = MutableSharedFlow<Int>()
val event3 = _event3.asSharedFlow()

 

UI

lifecycleScope.launch {
    viewModel.event1.collect { text ->
        // TODO
    }
    
    viewModel.event2.collect { text ->
        // TODO
    }
    
    viewModel.event3.collect { text ->
        // TODO
    }
}

 

수천, 수백개의 이벤트를 처리를 해야한다고 하면

이런 코드를 지양할 필요가 있습니다.

 

 

2. Sealed Class Event() 클래스로 이벤트 처리하기

해결하기 위해서는 아래처럼 로직을 작성해야합니다.

1) 이벤트를 Sealed Class로 상황에 맞게 처리하게 만들고

2) sharedFlow 1개로 이벤트를 전달

3) flow를 collect로 받고 분기처리 하기

 

viewModel

class ViewModel : ViewModel() {

    private val _flow = MutableSharedFlow<Event>()
    val flow = _flow.asSharedFlow()

    fun showName() {
        event(Event.Event1("xhan"))
    }

    fun showMarried() {
        event(Event.Event2(false))
    }

    fun showAge() {
        event(Event.Event3(28))
    }

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

    sealed class Event {
        data class Event1(val name: String) : Event()
        data class Event2(val married: Boolean) : Event()
        data class Event3(val age: Int) : Event()
    }
}

 

lifecycleScope.launch {
    viewModel.flow.collect { event -> handleEvent(event) }
}

private fun handleEvent(event: Event) = when (event) {
    is Event.ShowName -> // TODO
    is Event.ShowMarried -> // TODO
    is Event.ShawAge -> // TODO
}

 

근데 sealed class를 사용하는 이유는 이벤트를 else로 분기 처리되는 경우를 없애려고 해서일까?(아시는 분들은 댓글 쫌)

 

3. 데이터 변경에 따른 주기적인 UI 변경

하지만 위에 방식도 생명주기 관리 부분에서 문제가 있습니다.

1) 서버에서 가지고 오는 데이터가 실시간으로 변경되어서 UI가 변경될 경우

2) 백그라운드에 있어서 UI를 계속 그려줄 필요가 없는 경우

 

위에 문제는 flow 데이터를 

onStart()에서 collect를 시작하고, onStop()에서 cancel 하면 해결할 수 있죠?

class LocationActivity : AppCompatActivity() {

    // Coroutine listening for Locations
    private var locationUpdatesJob: Job? = null

    override fun onStart() {
        super.onStart()
        locationUpdatesJob = lifecycleScope.launch {
            viewModel.locationFlow().collect {
                // New location! Update the map
            } 
        }
    }

    override fun onStop() {
        // Stop collecting when the View goes to the background
        locationUpdatesJob?.cancel()
        super.onStop()
    }
}

 

4. repeatOnLifeCycle() 함수 사용하기

flow 데이터를 onStart()에서 collect하고 onStop()에서 따로 종료해주는 방식은 매번 보일러 플레이트 코드를 만들게 됩니다.

이러한 Flow의 불필요한 메모리 사용을 방지하며 보일러 플레이트를 줄일 수 있는 방법이 있습니다.

그것은 바로 AAC의 LifeCycle의 repeatOnLifeCycle!! (자세한 내용은 블로그 참조: https://kotlinworld.com/228)

 

repeatOnLifeCycle을 적용해서 만든다고 하면 아래와 같은 코드가 됩니다.

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

        // 1. lifecycleScope 생성
        lifecycleScope.launch {

            // 2. repeatOnLifecycle로 새로운 코루틴 반복 생성과 취소
            repeatOnLifecycle(Lifecycle.State.STARTED) {
            
                // 3. STARTED 생명주기 시작
                viewModel.flow().collect {
                     // 4. 지속적인 UI 업데이트 처리 
                }
            }
            // 5. DESTROYED 생명주기 종료
        }
    }

}

 

 

5. repeatOnLifeCycle() 확장함수로 만들기

위에서 썼던 lifeCycleScope안에 repeatOnLifeCycle안에 flow를 collect() 하는 방식을

확장함수로 사용한다면 1줄로 처리할 수 가 있습니다.

 

아래와 같이 확장 함수를 만들 수 있습니다.

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

 

확장함수 사용 이전

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

 

확장함수 사용 이후

코드가 간결해졌고 재사용성도 향상되었겠지요?

repeatOnStarted {
    viewModel.eventFlow.collect { event -> handleEvent(event) }
}

 

아래는 전체 코드입니다.

class Activity : BaseActivity<ActivityBinding, ViewModel>(R.layout.activity) {

    override val viewModel: ViewModel by viewModels()

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

        repeatOnStarted {
            viewModel.flow.collect { event -> handleEvent(event) }
        }
    }

    private fun handleEvent(event: Event) = when (event) {
        is Event.EVENT1 -> // TODO
        is Event.EVENT2 -> // TODO
        is Event.EVENT3 -> // TODO
    }
}

 

 

6. 데이터를 소비하기 전 onStop()된다면?

위에 코드에도 문제점이 있습니다.

서버에서 데이터를 다 가져오지 못한 상태에서 홈버튼을 눌러 백그라운드로 이동한다면

onStop()된 상태여서 event를 제대로 받을 수 가 없습니다.

 

그래서 Ted Park이 만든 EventFlow를 사용한다면 이 문제를 해결할 수 있습니다.

EventFlow의 로직은 아래와 같습니다.

 

1. Event가 발생했을 때 캐시한 후

2. 새로 구독하는 observer가 있다면 캐시한 Event를 전파

 

이렇게 한다면 데이터를 다 받지 못한 상태에서 onStop()되어도

다시 앱에 들어와 onStart()로 들어와서 event를 실제 소비할 때 

캐시로 저장한 Event를 받을 수 있게 만들어둔 것 같습니다!!(갓상권)

 

EvnetFlow.kt

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)
}

 

viewModel

private val _eventFlow = MutableEventFlow<Event>()
val eventFlow = _eventFlow.asEventFlow()

(최최최최최종판)

 

해당 EventFlow까지 사용했다면 viewModel에서 이벤트 처리하는 건

완성형이라고 볼 수 있을 거 같습니다.

 

 

이 글을 전달해주신 저희 팀장님께 감사드리며

안드로이드 근본 Ted Park에게 절 올립니다.

감사합니다.

 

 

 

Comments