Lifecycle-Aware하게 이벤트 처리하기

동키·2025년 4월 24일

안드로이드

목록 보기
11/14

다들 개발하시며 이벤트 처리를 어떤 방법으로 사용하시고 계신가요?

저는 아래의 7가지 방법에서 4번 방법을 사용하다 최근 repeatOnLifecycle 개념에 대해 알게되어 5번 방법을 사용하고 있었습니다. 7가지 방법에 대해 궁금하시다면 Ted Park 님의 블로그를 확인해 보시기 바랍니다.

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

내가 최근 사용한 이벤트 처리

// XML
 lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.eventFlow.collect { event -> handleEvent(event) }
            }
        }

// Compose
 val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(lifecycleOwner) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            uiEvent.collect { event ->
		            handleEvent(event)
            }
        }
    }

하지만 SharedFlow를 통해 Event 처리 시 문제가 발생할 수 있다는 사실을 Ted Park님의 글을 보고 알게되었습니다.

만약 사용자가 리스트에서 item 하나를 클릭했고 서버로부터 상태를 체크하고 화면을 보여주는 로직이 있다고 가정했을 때 아직 서버로부터 상태 체크가 끝나지 않았는데 사용자가 Home 버튼을 눌러 앱이 백그라운드로 내려갔다면 상세 화면을 실행하는 이벤트를 emit 해도 onStop 상태에 있기 때문에 이벤트를 받지 못합니다.

그럼 SharedFlow의 reply 쓰면 되는거 아닌가? 할 수 있지만

reply를 설정하게 되면 Configuration Changer가 일어났을 때 이전 이벤트를 받게되어 의도하지 않은 동작을 일으키게 됩니다. (화면 전환 → 토스테 메시지 나옴)

이를 방지하기 위해 EventFlow 를 직접 작성하여 reply로 이전 이벤트들을 가지고 있다가 새로운 구독자가 생겼을 때
consumed (소비) 소비되지 않은 이벤트라면 값을 emit 하고 이미 소비된 이벤트라면 값을 방출하지 않습니다.

EventFlow에 대한 자세한 내용은

위의 Ted Park 님의 포스팅에서 자세한 내용을 확인하실 수 있습니다.

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

Channel을 이용한 이벤트 처리

EventFlow 에 감탄하고 있던 도중 Ted Park님의 블로그에서

ViewModel에서 더이상 EventFlow를 사용하지 마세요

이러한 내용의 글을 읽게 되었습니다. 또 감탄…

뷰모델에서 이벤트를 전파할때 필요한 조건

  1. 구독자가 없을 때 이벤트가 발생했어도, 다시 구독자가 생기면 해당 이벤트가 전달되어야 합니다. 위에서 든 예시로 사용자가 이벤트를 받기 전 홈버튼을 누르고 다시 돌아왔을 때 해당 이벤트를 전달 받을 수 있어야 합니다.
  2. 이벤트가 소비되면(처리) 다시 처리되지 않아야 합니다. Configufation Change가 일어나도 이전에 소비된 이벤트는 더 이상 받을 필요가 없습니다.

이러한 규칙을 EventFlow 도 잘 지키지만 Channel 또한 이 요건을 다 충족합니다.

channel은 비동기 데이터 스트림이라고 생각하시면 됩니다.

자세한 내용은 위 블로그 링크를 참고해주세요.

Channel 사용시 직접 EventFlow를 구현하지 않아도 우리가 원하는 이벤트 처리가 가능합니다.

Channel의 *receiveAsFlow 로* Flow로 만들 수 있기 때문에 뷰에서는 바뀔 코드가 없고 뷰모델의 코드만 수정하면 됩니다.

  // viewModel
  private val _uiEvent = Channel<SignUpUiEvent>()
  val uiEvent = _uiEvent.receiveAsFlow()

Ted Park님이 제공해주신 샘플코드에서

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

LifecycleOwner의 확장함수를 작성해서 계속해서 lifecycleScope.launch 안에 repeatOnLifecycle을 사용하지 않도록 사용하신 모습을 볼 수 있습니다. 중복 코드를 없애고 액티비티에서도 간편하게 사용할 수 있습니다.

 // Activity
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        repeatOnStarted {
            viewModel.eventFlow.collect { event -> handleEvent(event) }
        }
    }

Compose 에서는?

Compose에서도 항상 LaunchedEffect를 작성하고 lifecycleOwner.lifecycle.repeatOnLifecycle 을 계속 작성해야 하는 번거로움이 있다고 생각해 확장 함수를 작성해봤습니다.

@Composable
fun <T> Flow<T>.collectOnStarted(
    action: suspend (T) -> Unit
) {
    val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(this, lifecycleOwner) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            Log.d("GuestDetailViewModel", "collectOnStarted")
            collect { action(it) }
        }
    }
}

EventFlow , Channel.receiveAsFlow() 모두 Flow 타입을 만족하기 때문에 Flow에 대한 확장 함수를 작성했습니다.

lifecycleOwner값을 가져오고 repeatOnLifecycle을 사용해 Lifecycle의 상태가 STARTED 상태일 때만 이벤트를 수집하고 ONSTOP 상태일 때는 수집되지 않게 합니다.

확장 함수를 사용한 뷰 코드입니다.

@Composable
fun MyScreen(
	viewModel: TestViewModel = hiltViewModel()
) {
	viewModel.uiEvent.collectOnStarted { event->
		when(event) {...}
	}
}
profile
오늘 하루도 화이팅

0개의 댓글