Skip to content

⚙ [트러블 슈팅] SnackBar UI 상태

Moony-H edited this page Dec 5, 2024 · 11 revisions

⚠️ 문제 상황

snackBar를 호출하는 과정에서 너무나 많은 요청이 있을 경우 계속해서 스낵바가 뜨는 경우가 있었다.

snackBar-ezgif.com-crop-video.mov




또한 MainScreen에서 SnackBar를 처리하고 있기 때문에 하위 스크린에서 SnackBar를 실행시키기 위해서는

Props Drilling으로 Lambda를 함수의 인자로 계속해서 전달해 주어야 했다.



Untitled (23)



❓고민거리

  1. SnackBar Lambda를 Props Drilling 하지 않고 바로 접근할 수 없을까?

  2. 앱에서 일어나는 SnackBar 이벤트를 하나의 Screen에서 모두 일괄처리 할 수 있을까?

  3. 기타 모든 곳에서 SnackBar에 자유롭게 접근할 수 없을까?

이를 해결하기 위해 생각한 방법은 아래와 같다

1.SnackBar Lambda를 Props Drilling 하지 않고 바로 접근할 수 없을까?

3.기타 모든 곳에서 SnackBar에 자유롭게 접근할 수 없을까?

-> CompositionLocal을 사용하여 current로 접근 가능하게 하자!



  1. 앱에서 일어나는 SnackBar 이벤트를 하나의 Screen에서 모두 일괄처리 할 수 있을까?

-> MainViewModel에 SharedFlow를 넣어 모든 SnackBar 이벤트를 순서대로 넣어 MainScreen에서 관찰하여 일괄처리하자!



결과적으로 아래와 같은 구조를 생각해냈다.

Untitled (24)


✔️ 문제 해결

먼저 SnackBar에게 전달할 데이터인 SnackBarEvent를 만들었다.

우리 서비스에서는 Login화면으로 이동, 혹은 단순 메시지 SnackBar만 존재하기 때문에 두 가지 타입을 Sealed class로 만들었다.

스크린샷 2024-12-05 오전 10 55 59

sealed class SnackBarEvent {
    abstract val message: String
    abstract val actionLabel: String?

    class LoginRequired : SnackBarEvent() {
        override val message: String = "로그인 후 이용 가능한 서비스입니다."
        override val actionLabel: String = "로그인"
    }

    data class Message(
        override val message: String,
        override val actionLabel: String?,
    ) : SnackBarEvent()
}



그 다음 SnackBar의 Event를 ViewModel에 전달하기 위한 class SnackBarBridge를 만들었다.


스크린샷 2024-12-05 오전 10 53 43


class SnackBarBridge(
    private val onSnackBarDataAdded: (SnackBarEvent) -> Unit
) {
    fun postSnackBarEvent(event: SnackBarEvent) {
        onSnackBarDataAdded(event)
    }
    fun postSnackBarString(message: String) {
        onSnackBarDataAdded(SnackBarEvent.Message(message, null))
    }
}



SnackBarHostState에 위의 데이터를 사용하여 SnackBar를 표시하는 Extension 함수와 CompositionLocalProvider에 들어갈 compositionLocal 변수를 만들었다.

스크린샷 2024-12-05 오전 10 56 48

val LocalSnackBarBridge = compositionLocalOf<SnackBarBridge> { error("No SnackBarHostState provided") }

suspend fun SnackbarHostState.showSnackBarWithData(data: SnackBarEvent) =
    showSnackbar(
        message = data.message,
        actionLabel = data.actionLabel,
        duration = SnackbarDuration.Short
    )



이제 데이터를 담을 SharedFlow를 ViewModel에 만들어야 한다.



SnackBar의 데이터를 최대 2개 까지만 보여주고 싶었다.

그럼 현재 보여지고 있는 SnackBar 1개와, 그 사이에 들어온 SnackBar Data 1개.

즉, 대기 큐에는 1개만 들어가야 한다.

그래서 SharedFlow에 extraBufferCapacity를 1로 놓고, 새로운 데이터가 들어오면 아직 보여주지 않은 낡은 데이터를 없애기 위해 BufferOverflow.DROP_OLDEST를 해 주었다.

스크린샷 2024-12-05 오전 11 27 06

    private val _snackBarFlow = MutableSharedFlow<SnackBarEvent>(
        replay = 0,//새로운 구독자가 생기면 재발행 할 데이터 (재발행 안함)
        extraBufferCapacity = 1,//버퍼의 크기(대기큐. 기획상 1개만)
        onBufferOverflow = BufferOverflow.DROP_OLDEST//(새로운 데이터가 들어올 시 낡은 데이터를 버퍼에서 제거)
    )
    val snackBarFlow = _snackBarFlow.asSharedFlow()

    fun postSnackBarData(data: SnackBarEvent) {
        viewModelScope.launch {
            _snackBarFlow.emit(data)
        }
    }



모든 준비는 끝났다.

이제 MainScreen에 세팅을 해 주어

모든 곳에서 MainViewModel의 SnackBar SharedFlow에 데이터를 저장할 수 있게 하자.

스크린샷 2024-12-05 오후 12 54 18

//앱 전체에서 사용할 수 있도록 CompositionLocalProvider로 래핑
PorringTheme(isLightBars = isLightBars) {
    CompositionLocalProvider(LocalSnackBarBridge.provides(snackBarBridge)) {
        MainScreen(
            navigator = navigator,
            mainViewModel
        )
    }
}



결과:

여러번 클릭해도 SnackBar는 최대 두번만 뜨게 된다.

2024-12-051.02.44-ezgif.com-crop-video.mov
Clone this wiki locally