Android Compose 전역으로 사용할 수 있는 스낵바

송규빈·2024년 8월 23일
1
post-thumbnail

개요

앱 어디에서나 스낵바를 표시할 수 있는 전역으로 사용가능한 스낵바에 대해 말하고자 한다.
구현 방법에 차이는 많겠지만, 아마도 스낵바를 사용하다보면 보일러플레이트 코드가 많아지는 경우를 겪을 수 있다.

그래서 이것을 해결할 수 있는 방법을 알아봤다.

Snackbar Controller

전역으로 편리하게 사용할 수 있게끔 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

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을 사용하여 이벤트를 관리한 이유는 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로 만들어줘야 한다.

OvserveEvents

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

LaunchedEffect

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 전송된 보류 중인 작업을 즉시 처리할 수 있게한다.

UI 단 코드

				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을 통해 수행하여도 된다.
자세한 내용은 샘플 코드를 참고하면 된다.

샘플 코드

https://github.com/Songgyubin/GlobalSnackbarSample

profile
🚀 상상을 좋아하는 개발자

0개의 댓글