앱 어디에서나 스낵바를 표시할 수 있는 전역으로 사용가능한 스낵바에 대해 말하고자 한다.
구현 방법에 차이는 많겠지만, 아마도 스낵바를 사용하다보면 보일러플레이트 코드가 많아지는 경우를 겪을 수 있다.
그래서 이것을 해결할 수 있는 방법을 알아봤다.
전역으로 편리하게 사용할 수 있게끔 object로 Snackbar Controller를 하나 만들어준다.
data class SnackbarEvent(
val message: String,
val action: SnackbarAction? = null,
)
data class SnackbarAction(
val name: String,
val action: suspend () -> Unit,
)
object SnackbarController {
private val _events = Channel<SnackbarEvent>()
val events = _events.receiveAsFlow()
suspend fun sendEvent(event: SnackbarEvent) {
_events.send(event)
}
}
SnackbarEvent와 SnackbarAction로 스낵바에 보여줄 메시지나 스낵바를 통해 실행할 action을 정의할 수 있게한다.
action은 사용하지 않는 경우도 더러 있기에 선택 사항으로 nullable로 세팅해준다.
data class SnackbarEvent(
val message: String,
val action: SnackbarAction? = null,
)
data class SnackbarAction(
val name: String,
val action: suspend () -> Unit,
)
channel을 사용하여 이벤트를 관리한 이유는 channel은 기본적으로 이벤트를 버퍼링하기 때문에 수신자가 없어도 이벤트가 손실되지 않기 때문이다.
그 후 receiveAsFlow()
를 사용하여 channel을 flow로 노출시켜서 UI에서 이를 관찰할 수 있게 한다.
private val _events = Channel<SnackbarEvent>()
val events = _events.receiveAsFlow()
관찰할 수 있는 환경을 만들어놨으니 이벤트를 채널로 전송할 수 있게 메서드를 만들어준다.
suspend fun sendEvent(event: SnackbarEvent) {
_events.send(event)
}
send 메서드는 suspend fun이므로 sendEvent도 suspend로 만들어줘야 한다.
@Composable
fun <T> ObserveEvents(
flow: Flow<T>,
key1: Any? = null,
key2: Any? = null,
onEvent: (T) -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(lifecycleOwner.lifecycle, key1, key2, flow) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
withContext(Dispatchers.Main.immediate) {
flow.collect(onEvent)
}
}
}
}
이제 UI 레이어에서 이벤트를 간편하고 효율적으로 관찰할 수 있고, 모든 유형의 이벤트에서 동작할 수 있도록 제네릭 함수를 만들어 준다.
또한 key에 대해서도 Any?를 통해 세팅해준다.
그 후에는 이벤트를 수신할 때 트리거되는 람다도 만들어준다.
@Composable
fun <T> ObserveEvents(
flow: Flow<T>,
key1: Any? = null,
key2: Any? = null,
onEvent: (T) -> Unit
)
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(lifecycleOwner.lifecycle, key1, key2, flow) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
withContext(Dispatchers.Main.immediate) {
flow.collect(onEvent)
}
}
}
LaunchedEffect
의 key를 lifecycleOwner.lifecycle
로 설정함으로써 수명 주기가 변경될 때마다 새로운 수명 주기로 시작된 LaunchedEffect
를 다시 호출하게 한다.
또한 컴포즈 상태가 변경될 수도 있고, flow가 변경될 수도 있으므로 매개변수로 받은 각 key와 flow도 설정한다.
repeatOnLifecycle(Lifecycle.State.STARTED)
을 사용하여 생명주기가 Start
상태에 있을 때 호출되도록 한다.
즉, 화면이 회전한 후나 Activity를 처음으로 부팅할 때 호출된다.
Main.immediate
전송된 보류 중인 작업을 즉시 처리할 수 있게한다.
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
ObserveEvents(
flow = SnackbarController.events,
key1 = snackbarHostState
) { event ->
coroutineScope.launch {
snackbarHostState.currentSnackbarData?.dismiss()
val result = snackbarHostState.showSnackbar(
message = event.message,
actionLabel = event.action?.name,
duration = SnackbarDuration.Short
)
if(result == SnackbarResult.ActionPerformed) {
event.action?.action?.invoke()
}
}
}
전송된 이벤트에 대한 내용을 snackbarHostState
를 사용하여 스낵바를 보여주도록 한다.
UI 단 코드는 일반 스낵바를 사용할 때와 비슷하다.
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState
)
}
) {
...
}
이벤트를 전송할 때는 뷰모델에서 수행해도 되고, Composable을 통해 수행하여도 된다.
자세한 내용은 샘플 코드를 참고하면 된다.