최근 프로젝트에서 ViewModel과 Activity 사이의 UI 이벤트 전달을 테스트하면서,
가끔 이벤트가 Activity에 도달하지 않는 문제를 발견했다.
테스트에서는 실패, 실제 앱에서도 일부 UI가 이상하게 동작하곤 했다.
// ViewModel
private val _eventChannel = Channel<UiEvent>(Channel.BUFFERED)
val eventFlow = _eventChannel.receiveAsFlow()
fun onExpandTextClick() {
viewModelScope.launch {
_eventChannel.send(UiEvent.ExpandText)
}
}
// Activity
lifecycleScope.launchWhenStarted {
viewModel.eventFlow.collect { event ->
when(event) {
UiEvent.ExpandText -> binding.description.maxLines = Int.MAX_VALUE
}
}
}
launchWhenStarted는 STARTED 이상일 때만 collect → 타이밍에 따라 이벤트 유실
Unit Test 에서는 이벤트를 수집하지 못함.
Channel은 즉시 소비 구조이다.
버퍼가 비어있거나 소비자가 준비되지 않으면 이벤트가 유실될 수 있다.
launchWhenStarted는 Activity의 STARTED 상태에서만 수집 → 이벤트를 놓치면 끝
즉, 타이밍과 수명주기 교차점에서 이벤트가 사라지는 것이다.
// UI 이벤트용 SharedFlow
private val _ui = MutableSharedFlow<UiEvent>(replay = 1)
val ui = _ui.asSharedFlow()
fun onExpandTextClick() {
_ui.tryEmit(UiEvent.ExpandText) // 즉시 전달, 유실 방지
}
replay = 1 → 늦게 구독해도 마지막 이벤트 보존한다.
tryEmit → 코루틴 블록 밖에서도 즉시 이벤트 전달한다.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.ui.collect { event ->
when(event) {
UiEvent.ExpandText -> {
binding.description.maxLines = Int.MAX_VALUE
binding.showMore.visibility = View.GONE
}
}
}
}
}
repeatOnLifecycle → STARTED 상태에서만 반복적으로 안전하게 collect
Activity가 일시정지됐다가 돌아와도 이벤트를 놓치지 않는다.
실패 테스트 (Channel)
@Test
fun `showMore 클릭 시 ExpandText 이벤트 발생`() = runTest {
val events = mutableListOf<UiEvent>()
val job = launch(UnconfinedTestDispatcher()) {
viewModel.eventFlow.toList(events)
}
viewModel.onExpandTextClick()
advanceUntilIdle()
assertTrue(events.contains(UiEvent.ExpandText)) // 가끔 실패
job.cancel()
}
성공 테스트 (SharedFlow)
@Test
fun `showMore 클릭 시 ExpandText 이벤트 발생`() = runTest {
val events = mutableListOf<UiEvent>()
val job = launch(UnconfinedTestDispatcher()) {
viewModel.ui.toList(events)
}
viewModel.onExpandTextClick()
advanceUntilIdle()
assertTrue(events.contains(UiEvent.ExpandText)) // 안정적으로 성공
job.cancel()
}
SharedFlow를 사용하면 Activity 수명주기와 상관없이 이벤트가 안전하게 전달된다.
UI 이벤트는 단발성 이벤트 → Channel보다는 SharedFlow/SingleLiveEvent 권장되고 있다.
수명주기와 이벤트 타이밍이 맞지 않으면 이벤트 유실이 발생한다.