CollectLatest를 사용해서 이벤트 처리 최적화를 해보자
달력에서 날짜를 선택할 때 이벤트를 처리해서 snackbar가 보이도록 하려고 했는데, 버튼을 연속으로 눌렀을 때 snackbar가 중첩이 되어서 시간이 딜레이로 계속 나오게 됩니다.
하지만 이럴 경우 메모리 사용도 발생하게 되고, 사실 마지막 값을 제외한다면 이전 선택은 불필요하므로 최신 값 기준으로 이벤트 처리를 할 수 있도록 개선하기로 하였습니다.
해당 작업을 진행하기 전에 Collect와 CollectLatest의 차이점을 알아야 합니다.
간단하게 설명하자면 Collect는 작업이 들어온 순서대로 처리하는 것이고 CollectLatest는 마지막에 들어온 최신 값만 처리하게 되는 방법입니다.
val snackbarFlow = MutableSharedFlow<String>()
ViewModel
에서 snackbar를 호출하기 위한 함수를 만들어두고 snackbarFlow.emit()
이 호출되면 @Composable에서 실행되게 하였습니다.
LaunchedEffect(recordViewModel.snackbarFlow) {
recordViewModel.snackbarFlow.collect { message ->
snackbarHostState.showSnackbar(message)
}
}
여기서 collect
가 사용되는 이를 collectLatest
로 사용함으로써 이전 snackbar 호출에 대한 이벤트를 모두 취소함으로써 최신 이벤트만 처리할 수 있게 됩니다.
그런데 collect
를 사용해도 MutableSharedFlow()
의 옵션을 사용해서 약간의 해결 방법이 있긴 합니다.
replay
와 onBufferOverflow
를 사용합니다.
replay
는 리플레이 버퍼 크기를 조절합니다. 즉, 여러 개의 이벤트가 들어와도 저장될 수 있는 크기를 정해놓는 겁니다.
onBufferOverflow
에서는 DROP_OLDEST
과 DROP_LATEST
옵션 2가지로 버퍼가 가장 찼을 때 오래된 값을 버리거나 가장 최근의 값을 버리거나 선택할 수 있습니다.
val snackbarFlow = MutableSharedFlow<String>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
여기서는 위처럼 옵션을 저장한다면 collect
를 사용하면서 최대한 자원을 아끼고 최신 이벤트를 보유할 수 있게 됩니다.
참고로 replay
를 0으로 설정하게 되면 에러가 발생하는데요
DROP_OLDEST
또는 DROP_LATEST
로 설정하려면 replay
또는 extraBufferCapacity
값이 양수여야 합니다.
이는 SharedFlow
가 어느 정도의 버퍼를 가지고 있어야 오버플로우 전략을 적용할 수 있기 때문입니다.
코드는 아래와 같습니다.
Composable Function
@Composable
private fun RecordCalendar(
calendarViewModel: RecordCalendarViewModel = hiltViewModel(), recordViewModel: RecordViewModel
) {
val calendarUiState by calendarViewModel.calendarUiState.collectAsState() // 달력 날짜 전체 데이터
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
SnackbarHost(hostState = snackbarHostState)
LaunchedEffect(recordViewModel.snackbarFlow) {
recordViewModel.snackbarFlow.collectLatest { message ->
snackbarHostState.showSnackbar(message)
}
}
Surface(
// modifier = Modifier.wrapContentHeight().verticalScroll(rememberScrollState()), color = White
modifier = Modifier.wrapContentHeight(), color = White
) {
CalendarWidget(days = DateUtil.daysOfWeek,
yearMonth = calendarUiState.yearMonth,
dates = calendarUiState.dates,
onPreviousMonthButtonClicked = { prevMonth ->
calendarViewModel.toPreviousMonth(prevMonth)
},
onNextMonthButtonClicked = { nextMonth ->
calendarViewModel.toNextMonth(nextMonth)
},
onDateClickListener = {
coroutineScope.launch {
recordViewModel.showSnackbar("날짜 선택")
}
})
}
} // End of RecordCalendar()
ViewModel
@HiltViewModel
class RecordViewModel @Inject constructor() : ViewModel() {
// val snackbarHostState = SnackbarHostState()
val snackbarFlow = MutableSharedFlow<String>()
fun showSnackbar(text: String) {
viewModelScope.launch {
snackbarFlow.emit(text)
}
}
} // End of RecordViewModel class
Collect를 사용하면 위 영상처럼 클릭 된 이벤트들이 누적되어 snackbar가 계속 나오게 된다.
CollectLatest를 사용하게 되면 최신 이벤트를 기준으로 처리하여 snackbar가 한 번만 표시되고 더는 나오지 않게 된다.