🍫 Custom Snack Bar λ₯Ό λ§Œλ“€μ–΄λ³΄μž ~

RyanΒ·2025λ…„ 12μ›” 3일

Android

λͺ©λ‘ 보기
2/3
post-thumbnail

1. [기획 μ˜λ„] λ‚΄κ°€ μ–΄λ–€ κΈ°λŠ₯을 μ–΄λ–»κ²Œ λ™μž‘ν•˜λ„λ‘ κ΅¬ν˜„ν•˜κ³ μž ν–ˆμ—ˆλŠ”λ°,

μ €λŠ” μ΅œκ·Όμ— μŠ€λ‚΅λ°”λ₯Ό κ΅¬ν˜„ν•˜λ©΄μ„œ 각각의 ν™”λ©΄μ—μ„œ νŠΉμ • λ²„νŠΌ 클릭 μ‹œ μŠ€λ‚΅λ°”κ°€ 3초 λ™μ•ˆ λ…ΈμΆœλ˜κ³ , "이동" λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ νŠΉμ • ν™”λ©΄μœΌλ‘œ μ΄λ™ν•˜λŠ” κΈ°λŠ₯이 ν•„μš”ν–ˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ μ‹€μ œλ‘œ κ΅¬ν˜„μ„ μ‹œμž‘ν•˜μžλ§ˆμž 생각보닀 λ§Žμ€ λ¬Έμ œκ°€ λ“±μž₯ν–ˆμŠ΅λ‹ˆλ‹€.

이 κΈ€μ—μ„œλŠ”

"Compose의 Snackbarκ°€ μ™œ 3초 κ³ μ • μ‹œκ°„μ΄ μ•ˆ λ˜λŠ”μ§€?"
"μ™œ 연속 클릭 μ‹œ μŠ€λ‚΅λ°”κ°€ 반볡 ν˜ΈμΆœλ˜λŠ”μ§€?"
"μ–΄λ–»κ²Œ μ»€μŠ€ν…€ μŠ€λ‚΅λ°”λ₯Ό μ•ˆμ •μ μœΌλ‘œ κ΅¬ν˜„ν–ˆλŠ”μ§€?"

μœ„μ— λŒ€ν•œ ν•΄κ²° 과정을 기둝해보렀고 ν•©λ‹ˆλ‹€.

λͺ…μ„ΈλŠ” λ‹€μŒκ³Ό 같이 λ˜μ–΄ μžˆμ—ˆμ–΄μš”.

κΈ°λŠ₯을 정리해 보자면

  • κ³ μ • ν…μŠ€νŠΈ
  • β€œμ΄λ™β€ ν…μŠ€νŠΈ ν΄λ¦­μ‹œ νŽ˜μ΄μ§€ 이동
  • 3μ΄ˆκ°„ μŠ€λ‚΅λ°”κ°€ λ„μ›Œμ Έ μžˆμ–΄μ•Όν•¨

2. [문제 νŒŒμ•…] μ–΄λ–€ λ¬Έμ œκ°€ λ°œμƒν–ˆκ³ ,

λ¨Όμ € μŠ€λ‚΅λ°”λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄μ„œ κ³΅μ‹λ¬Έμ„œλ₯Ό 확인해 λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

λ°‘μ—μ„œ ν•˜λ‚˜ν•˜λ‚˜μ”© λœ―μ–΄ λ³Όκ²Œμš”..!

SnackBar λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄μ„œλŠ” 크게 3κ°€μ§€ μž¬λ£Œκ°€ ν•„μš”ν–ˆμŠ΅λ‹ˆλ‹€.

  1. SnackBar UI(μŠ€λ‚΅λ°” 컴포저블 ν•¨μˆ˜ UI)

    μŠ€λ‚΅λ°” 컴포저블 ν•¨μˆ˜ UI

    μ‹€μ œλ‘œ androidx.compose.material3 μ—μ„œ Snackbar μ»΄ν¬λ„ŒνŠΈλ₯Ό μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

  • μ½”λ“œ

    @Composable
    fun Snackbar(
        modifier: Modifier = Modifier,
        action: @Composable (() -> Unit)? = null,
        dismissAction: @Composable (() -> Unit)? = null,
        actionOnNewLine: Boolean = false,
        shape: Shape = SnackbarDefaults.shape,
        containerColor: Color = SnackbarDefaults.color,
        contentColor: Color = SnackbarDefaults.contentColor,
        actionContentColor: Color = SnackbarDefaults.actionContentColor,
        dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
        content: @Composable () -> Unit,
    ) {
        Surface(
            modifier = modifier,
            shape = shape,
            color = containerColor,
            contentColor = contentColor,
            shadowElevation = SnackbarTokens.ContainerElevation,
        ) {
            val textStyle = SnackbarTokens.SupportingTextFont.value
            val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
            CompositionLocalProvider(LocalTextStyle provides textStyle) {
                when {
                    actionOnNewLine && action != null ->
                        NewLineButtonSnackbar(
                            text = content,
                            action = action,
                            dismissAction = dismissAction,
                            actionTextStyle = actionTextStyle,
                            actionContentColor = actionContentColor,
                            dismissActionContentColor = dismissActionContentColor,
                        )
                    else ->
                        OneRowSnackbar(
                            text = content,
                            action = action,
                            dismissAction = dismissAction,
                            actionTextStyle = actionTextStyle,
                            actionTextColor = actionContentColor,
                            dismissActionColor = dismissActionContentColor,
                        )
                }
            }
        }
    }

    ν•˜μ§€λ§Œ, μ΄λŠ” shadowElevation 을 κ³ μ •κ°’μœΌλ‘œ μ‚¬μš©ν•˜κ³  μžˆκΈ°μ— μ»€μŠ€ν…€ν•œ SnackBar λ₯Ό λ§Œλ“€κΈ°μ—λŠ” λΆ€μ ν•©ν•˜λ‹€κ³  생각해 직접 컴포저블을 κ΅¬μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

  • MelonActionSnackbar μ½”λ“œ

    @Composable
    fun Snackbar(
        modifier: Modifier = Modifier,
        action: @Composable (() -> Unit)? = null,
        dismissAction: @Composable (() -> Unit)? = null,
        actionOnNewLine: Boolean = false,
        shape: Shape = SnackbarDefaults.shape,
        containerColor: Color = SnackbarDefaults.color,
        contentColor: Color = SnackbarDefaults.contentColor,
        actionContentColor: Color = SnackbarDefaults.actionContentColor,
        dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
        content: @Composable () -> Unit,
    ) {
        Surface(
            modifier = modifier,
            shape = shape,
            color = containerColor,
            contentColor = contentColor,
            shadowElevation = SnackbarTokens.ContainerElevation,
        ) {
            val textStyle = SnackbarTokens.SupportingTextFont.value
            val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
            CompositionLocalProvider(LocalTextStyle provides textStyle) {
                when {
                    actionOnNewLine && action != null ->
                        NewLineButtonSnackbar(
                            text = content,
                            action = action,
                            dismissAction = dismissAction,
                            actionTextStyle = actionTextStyle,
                            actionContentColor = actionContentColor,
                            dismissActionContentColor = dismissActionContentColor,
                        )
                    else ->
                        OneRowSnackbar(
                            text = content,
                            action = action,
                            dismissAction = dismissAction,
                            actionTextStyle = actionTextStyle,
                            actionTextColor = actionContentColor,
                            dismissActionColor = dismissActionContentColor,
                        )
                }
            }
        }
    }

    Snackbar 에 λ“€μ–΄κ°ˆ message, μ•‘μ…˜ λ²„νŠΌμ˜ ν…μŠ€νŠΈμΈ actionLabel, μ‹€μ œ μ–΄λ–€ λ™μž‘μ„ ν• μ§€ action ν•¨μˆ˜λ₯Ό λ°›μ„μˆ˜ μžˆλ„λ‘ κ΅¬μ„±ν•΄λ³΄μ•˜μ–΄μš”.

  1. SnackBarHost
    Scaffoldμ—μ„œ μ‚¬μš©λ˜λŠ” Snackbar듀을 ν‘œμ‹œν•˜κΈ° μœ„ν•œ Host둜,
    Material μŠ€νŽ™κ³Ό hostState에 따라 Snackbarλ₯Ό μ μ ˆν•œ μ‹œμ μ— 보여주고 숨기고 λ‹«λŠ” 역할을 ν•©λ‹ˆλ‹€.
  • μ½”λ“œ
    @Composable
    fun SnackbarHost(
        hostState: SnackbarHostState,
        modifier: Modifier = Modifier,
        snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) },
    ) {
        val currentSnackbarData = hostState.currentSnackbarData
        val accessibilityManager = LocalAccessibilityManager.current
        LaunchedEffect(currentSnackbarData) {
            if (currentSnackbarData != null) {
                val duration =
                    currentSnackbarData.visuals.duration.toMillis(
                        currentSnackbarData.visuals.actionLabel != null,
                        accessibilityManager,
                    )
                delay(duration)
                currentSnackbarData.dismiss()
            }
        }
        FadeInFadeOutWithScale(
            current = hostState.currentSnackbarData,
            modifier = modifier,
            content = snackbar,
        )
    }
    SnackBarHostState 와 snackbar 컴포저블 ν•¨μˆ˜λ₯Ό λ°›μ•„μ„œ 이λ₯Ό λ„μ›Œμ£ΌλŠ” 역할을 ν•©λ‹ˆλ‹€ !
  1. SnackBarHostState
    μŠ€λ‚΅λ°”μ˜ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³ , showSnackbar()λ₯Ό 톡해 μŠ€λ‚΅λ°”λ₯Ό λ„μ›λ‹ˆλ‹€.

    보톡 remember λ₯Ό μ΄μš©ν•΄μ„œ μ €μž₯ ν•œ ν›„, Scaffold 에 SnackBarHost λ₯Ό ꡬ성할 λ•Œ μ‚¬μš©ν•΄μš”.

    μŠ€λ‚΅λ°”λ₯Ό μ‘°μž‘ν•˜λŠ” 데 핡심인 μƒνƒœλ‘œ, 주둜 μ‚΄νŽ΄λ³Ό 곳은 Mutex() λ₯Ό μ΄μš©ν•œ 방식과 SnackbarData, showSnackbar() μž…λ‹ˆλ‹€.

  • Mutex() 와 showSnackBar()
    Snackbar μš”μ²­ 즉, λ„μš°λŠ” ν•¨μˆ˜λŠ” SnackBarHostState μΈμŠ€ν„΄μŠ€κ°€ κ°€μ§€κ³  μžˆλŠ”
    showSnackBar() λ₯Ό ν˜ΈμΆœν•΄μ„œ λ„μš°κ²Œ λ˜λŠ”λ°μš” ..!

    λ§Œμ•½, μ‚¬μš©μžκ°€ μ—¬λŸ¬λ²ˆ μŠ€λ‚΅λ°” μš”μ²­μ„ ν•˜κ²Œ 되면 μ–΄λ–»κ²Œ λ κΉŒμš”??
    μŠ€λ‚΅λ°”κ°€ λ™μ‹œμ— λ‘κ°œκ°€ 뜰 κ°€λŠ₯성은 쑴재 ν•˜μ§€ μ•Šμ„κΉŒμš”..??

    μŠ€λ‚΅λ°”λ₯Ό λ„μš°λŠ” ν•¨μˆ˜μΈ showSnackBar() λ‚΄λΆ€ μ½”λ“œλ₯Ό κ°„λ‹¨νžˆ 보자면 …

    suspend fun showSnackbar(visuals: SnackbarVisuals): SnackbarResult =
    		mutex.withLock {
    		    try {
    		        return suspendCancellableCoroutine { continuation ->
    		            currentSnackbarData = SnackbarDataImpl(visuals, continuation)
    		        }
    		    } finally {
    		        currentSnackbarData = null
    		    }
    }      

    주의 깊게 봐야할 점은 mutex.withLock λΆ€λΆ„μž…λ‹ˆλ‹€.

    mutex λŠ” 보톡 μ—¬λŸ¬ 코루틴이 곡유 μžμ›μ— λŒ€ν•΄ 데이터 무결성 ν˜Ήμ€ ν•˜λ‚˜μ˜ μž‘μ—…μ„ μ™„λ£Œν•˜λ„λ‘ 보μž₯ν• λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€
    withLock 블둝 μ•ˆμ— μžˆλŠ” μž‘μ—…μ„ μ™„μ „νžˆ μˆ˜ν–‰ν•˜κ²Œ ν•©λ‹ˆλ‹€. 이후 μž‘μ—…μš”μ²­μ΄ λ“€μ–΄μ˜€λ©΄ 이전 μž‘μ—…μ΄ μ™„λ£Œλ λ•ŒκΉŒμ§€ λŒ€κΈ°ν•œν›„ μ‹€ν–‰ν•©λ‹ˆλ‹€.

    즉, μ‚¬μš©μžκ°€ μ—°μ†μ μœΌλ‘œ 클릭을 ν•˜κ²Œ 되면 λ‘κ°œκ°€ λ™μ‹œμ— λœ¨λŠ” 것이 μ•„λ‹Œ, μž‘μ—…μ„ 큐처럼 μŒ“μ•„λ‘κ³  μŠ€λ‚΅λ°”κ°€ 사라지면 λ‹€μŒ μŠ€λ‚΅λ°”λ₯Ό λ„μ›Œμ£Όκ²Œ λ©λ‹ˆλ‹€ !!

  • showSnackbar()

    μ•žμ„œ λ³Έ showSnackbar() λ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ‹€μ œλ‘œ μ™ΈλΆ€μ—μ„œ μ‚¬μš©ν• λ•ŒλŠ” μ•„λž˜ μ½”λ“œλ₯Ό ν˜ΈμΆœν•΄μ„œ μ‚¬μš©ν•©λ‹ˆλ‹€.

    suspend fun showSnackbar(
        message: String,
        actionLabel: String? = null,
        withDismissAction: Boolean = false,
        duration: SnackbarDuration =              // duration κ³ μ •
            if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
    ): SnackbarResult =
            showSnackbar(SnackbarVisualsImpl(message, actionLabel, withDismissAction, duration))

    SnackbarDuration 은 Short, Long, Indefinite 3κ°€μ§€ duration λ§Œμ„ μ§€μ • ν• μˆ˜ 있게 λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.
    각각의 μ‹œκ°„μ€ 4초, 10초, λ¬΄ν•œ μœΌλ‘œλ˜μ–΄ μžˆμ–΄μ„œ 저희가 κ΅¬ν˜„ν•˜κ³ μž ν•˜λŠ” 3μ΄ˆλŠ” ν•΄λ‹Ή λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ ν•  수 μ—†μ—ˆμŠ΅λ‹ˆλ‹€ 😭😭

문제점 정리

이제 정리λ₯Ό ν•΄λ³΄μžλ©΄ ..!

  • elevation 고정값이기에, 직접 μ»€μŠ€ν…€ μ»΄ν¬λ„ŒνŠΈλ₯Ό κ΅¬ν˜„(ν•΄κ²°)
  • μŠ€λ‚΅λ°” μ—¬λŸ¬λ²ˆ ν΄λ¦­μ‹œλ§ˆλ‹€ 이전 μŠ€λ‚΅λ°”λŠ” μ·¨μ†Œν•˜κ³  μƒˆλ‘œμš΄ μŠ€λ‚΅λ°”λ§Œ λ™μž‘ν•˜λ„λ‘ κ΅¬μ„±ν•΄μ•Όν•œλ‹€.
    but, κΈ°μ‘΄ κ΅¬μ‘°λŠ” μŠ€λ‚΅λ°” λŒ€κΈ°κ°€ μŒ“μ΄λŠ” ꡬ쑰
  • snackbar duration 값을 3초둜 μ§€μ •
    but, κΈ°μ‘΄ κ΅¬μ‘°λŠ” μ‹œκ°„μ΄ 4초, 10초, λ¬΄ν•œμœΌλ‘œ μ§€μ •λ˜μ–΄ 있음.

3. [ν•΄κ²° κ³Όμ •] κ·Έ 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μ–΄λ–»κ²Œ μ ‘κ·Όν–ˆμœΌλ©°,

이제 ..! μ•žμ„œ 2κ°€μ§€ 문제λ₯Ό ν•΄κ²°ν•œ 방법에 λŒ€ν•΄ 이야기 ν•΄λ³Όκ²Œμš” γ…Žγ…Ž
μš°μ„  μ €λŠ” snackbar 에 λŒ€ν•œ λ‘œμ§μ„ λ”°λ‘œ λΆ„λ¦¬ν•˜κ³  μ‹Άμ—ˆκΈ°μ— SnackbarController λΌλŠ” 클래슀λ₯Ό λ§Œλ“€μ–΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€ !

  • MelonSnackbarController
    class MelonSnackbarController(
        private val coroutineScope: CoroutineScope,
    ) {
        val snackbarHostState = SnackbarHostState()
    
        var currentRequest by mutableStateOf<MelonSnackbarActionRequest?>(null)
            private set
            
        fun show(request: MelonSnackbarActionRequest) {
            currentRequest = request
    
            coroutineScope.launch {
                launch {
                    snackbarHostState.showSnackbar(
                        message = request.message,
                        actionLabel = request.actionLabel,
                        withDismissAction = true,
                        duration = SnackbarDuration.Short,
                    )
                }
            }
        }
    
        fun performAction() {
            currentRequest?.onClick?.invoke()
            currentRequest = null
    
            coroutineScope.launch {
                snackbarHostState.currentSnackbarData?.dismiss()
            }
        }
    }
    
    @Composable
    fun rememberMelonSnackbarController(
        scope: CoroutineScope = rememberCoroutineScope(),
    ): MelonSnackbarController =
        remember {
            MelonSnackbarController(scope)
        }
    
    showSnackbar λŠ” suspend ν•¨μˆ˜μ΄κΈ° λ•Œλ¬Έμ— rememberCoroutineScope λ₯Ό μ‚¬μš©ν•΄μ„œ λ™μž‘ν•˜λ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€. controller μžμ²΄λ„ 생성 ν›„ μ΄ˆκΈ°ν™” λ˜μ§€ μ•Šκ²Œ ν•˜κΈ° μœ„ν•΄ remember 둜 감싸 μ£Όμ—ˆμ–΄μš”.

λ‹€μŒμœΌλ‘œλŠ” Snackbar μš”μ²­μ„ μœ„ν•œ λͺ¨λΈκ³Ό μ™ΈλΆ€μ—μ„œ 트리거 ν•˜λŠ” λ°©μ‹μœΌλ‘œ μ‚¬μš©ν•˜κΈ° μœ„ν•΄ CompositonLoacal 을 μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

  • MelonSnackbarModel
    @Immutable
    data class MelonSnackbarActionRequest(
        val message: String,
        val actionLabel: String,
        val onClick: () -> Unit,
    )
    
    val LocalMelonSnackbarTrigger =
        staticCompositionLocalOf<(MelonSnackbarActionRequest) -> Unit> {
            error("No MelonSnackbarTrigger provided")
        }

μ‹€μ œ μ‚¬μš©ν•˜λŠ” κ³³μ—μ„œλŠ” snackbarController λ₯Ό ν˜ΈμΆœν•˜λŠ” λ‘œμ§μ„ trigger 둜 μ„ μ–Έν•˜μ—¬ μ™ΈλΆ€μ—μ„œ μ‚¬μš©ν•˜κ²Œ ν–ˆμŠ΅λ‹ˆλ‹€.

  • μ„ μ–Έ & μ‚¬μš©
    // MainScreen μ—μ„œ μ„ μ–Έ
    val snackbarController = rememberMelonSnackbarController()
    val snackbarTrigger: (MelonSnackbarActionRequest) -> Unit =
        remember {
            { request -> snackbarController.show(request) }
        }
    
    Scaffold(
        snackbarHost = {
            SnackbarHost(
                hostState = snackbarController.snackbarHostState,
            ) { data ->
                val request = snackbarController.currentRequest ?: return@SnackbarHost
    
                MelonActionSnackbar(
                    message = request.message,
                    actionLabel = request.actionLabel,
                    action = snackbarController::performAction,
                    modifier =
                        Modifier
                            .padding(
                                start = 16.dp,
                                end = 16.dp,
                                bottom = 16.dp,
                            ),
                )
            }
        },
      
    // μ™ΈλΆ€ Screen μ—μ„œ μ‚¬μš© ex. HomeScreen
    val snackbarTrigger = LocalMelonSnackbarTrigger.current
    val snackbarRequest =
        MelonSnackbarActionRequest(
            message = "mixup count $count",
            actionLabel = stringResource(snackbar_move_action_label),
            onClick = navigateToMixUp,
        )
    snackbarTrigger(snackbarRequest) // <- μ‹€μ œ ν˜ΈμΆœν•˜λŠ” ν•¨μˆ˜
    snackbarTrigger κ°€ 호좜이 되면 snackbarRequest κ°€ μ „λ‹¬λ˜κ³  이λ₯Ό 톡해 snackbarController.show κ°€ 호좜이 λ©λ‹ˆλ‹€.

λ‹€μ‹œ λŒμ•„μ™€μ„œ 보자면 ..!

  1. μŠ€λ‚΅λ°” μ—¬λŸ¬λ²ˆ ν΄λ¦­μ‹œλ§ˆλ‹€ 이전 μŠ€λ‚΅λ°”λŠ” μ·¨μ†Œν•˜κ³  μƒˆλ‘œμš΄ μŠ€λ‚΅λ°”λ§Œ λ™μž‘ν•˜λ„λ‘ ꡬ성

    κΈ°μ‘΄ κ΅¬μ‘°λŠ” 연속 ν΄λ¦­μ‹œ ν•΄λ‹Ή μŠ€λ‚΅λ°”μ— λŒ€ν•œ μš”μ²­μ΄ 사라지지 μ•Šκ³  λŒ€κΈ°ν–ˆλ‹€κ°€ κ³„μ†ν•΄μ„œ 이전 ν˜ΈμΆœμ„ λ‹€μ‹œ λΆˆλŸ¬μ™€μ§€λŠ”κ²ƒμ„ λ³Ό 수 μžˆλŠ”λ°μš”..!!

    μŠ€λ‚΅λ°” λ™μž‘ ꡬ성

    λ”°λΌμ„œ λͺ…μ‹œμ μœΌλ‘œ 이λ₯Ό ν•΄μ œν•˜κ²Œ ν•˜λŠ” 방법을 μ‚¬μš©ν–ˆμ–΄μš” !
    방법은 snackbarHostState 의 currentSnackbarData 에 dissmiss ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λ©΄λ˜λŠ”λ°μš” ..!

    μ•„λž˜λŠ” 맀번 ν˜ΈμΆœμ‹œ λ§ˆλ‹€ 이전 snackbar λ₯Ό λͺ…μ‹œμ μœΌλ‘œ ν•΄μ œν•΄μ£ΌλŠ” μ½”λ“œμž…λ‹ˆλ‹€.

    fun show(request: MelonSnackbarActionRequest) {
        snackbarHostState.currentSnackbarData?.dismiss() // λͺ…μ‹œμ μœΌλ‘œ Dismiss
    
        currentRequest = request
    
        coroutineScope.launch {
            launch {
                snackbarHostState.showSnackbar(
                    message = request.message,
                    actionLabel = request.actionLabel,
                    withDismissAction = true,
                    duration = SnackbarDuration.Short,
                )
            }
        }
    }
  2. snackbar duration 값을 3초둜 μ§€μ •

    그러면 !! 이제 μ‹œκ°„μ„ 쑰정해봐야 κ² μ£ ?? μ•žμ„œ 본것과 같이 Duration 을 Short 둜 μ§€μ •ν•  경우, κ°€μž₯ μ§§μ•„μ•Ό 4초의 μ‹œκ°„μ„ μ§€μ •ν•  수 μžˆμ—ˆμ–΄μš”

    λ”°λΌμ„œ Snackbar λ₯Ό λͺ…μ‹œμ μœΌλ‘œ 3초의 μ‹œκ°„μ΄ μ§€λ‚˜κ²Œ 되면 νƒ€μž„ 아웃이 걸리게 ν•˜μ—¬ μŠ€λ‚΅λ°”κ°€ dismiss λ˜λŠ” 둜직이 ν•„μš”ν–ˆμŠ΅λ‹ˆλ‹€

    ν•΄λ‹Ή 방식을 κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄μ„œ corutineScope 내뢀에 delay λ₯Ό λ‹΄λ‹Ήν•˜λŠ” launch 블둝을 λ§Œλ“€κ³  delay κ°€ μ§€λ‚˜λ©΄ dismiss ν•˜κ²Œ ν•˜μ˜€μ–΄μš”

  • μ½”λ“œ

    private const val SNACKBAR_AUTO_DISMISS_MS = 3000L
    
    fun show(request: MelonSnackbarActionRequest) {
        currentRequest = request
    
        coroutineScope.launch {
            snackbarHostState.currentSnackbarData?.dismiss()
    
            launch {
                snackbarHostState.showSnackbar(
                    message = request.message,
                    actionLabel = request.actionLabel,
                    withDismissAction = true,
                    duration = SnackbarDuration.Indefinite, // λ¬΄ν•œμœΌλ‘œ μ„€μ •
                )
            }
    
            launch {                                            
                delay(SNACKBAR_AUTO_DISMISS_MS)  // 3초 λ”œλ ˆμ΄    
                snackbarHostState.currentSnackbarData?.dismiss()
            }                                                   
        }
    }       

3초 λ”œλ ˆμ΄ μ½”λ“œ

ν•΄λ‹Ή μ½”λ“œλ‘œ μ‹€ν–‰ ν•΄λ³΄μ•˜λŠ”λ° λ­”κ°€ μ΄μƒν•˜μ£  ..?
λΆ„λͺ… 3μ΄ˆκ°€ μ§€λ‚˜κ³  λ‚˜μ„œ snackbar κ°€ μ‚¬λΌμ Έμ•Όν•˜λŠ”λ° μ™œ λ°”λ‘œ μ‚¬λΌμ§€λŠ” κ±ΈκΉŒμš”?????

λ¬Έμ œλŠ” 코루틴에 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 기쑴의 코루틴이 κ³„μ†ν•΄μ„œ λ™μž‘ν•˜κ³  μžˆμ—ˆκΈ° λ•Œλ¬ΈμΈλ°μš”

μ •λ¦¬ν•˜μžλ©΄...

  1. showSnackbar μˆ˜ν–‰ x 15
  2. λ§€ show λ§ˆλ‹€ delay 코루틴 μˆ˜ν–‰
  3. 각 코루틴 λ³„λ‘œ delay 3μ΄ˆκ°€ μ§€λ‚˜λ©΄ dismiss λ₯Ό 호좜
  4. ν˜„μž¬μ˜ μŠ€λ‚΅λ°”κ°€ dismiss λœλ‹€

λ„€ 이상 깑깑이의 μ‚½μ§ˆμ΄μ—ˆμŠ΅λ‹ˆλ‹€

그럼 이 문제λ₯Ό μ–΄λ–»κ²Œ ν•΄κ²° ν–ˆλƒλ©΄ ~
μ½”λ£¨ν‹΄μ˜ job.cancle()을 μ΄μš©ν•΄μ„œ μŠ€λ‚΅λ°” μš”μ²­μ— λŒ€ν•΄μ„œ 1λŒ€1둜 μ·¨μ†Œν•΄μ£ΌλŠ” 방식을 μ‚¬μš©ν–ˆμ–΄μš”.

  • μ½”λ“œ

    fun show(request: MelonSnackbarActionRequest) {
    
        coroutineScope.launch {
            snackbarHostState.currentSnackbarData?.dismiss()
    
            val job = launch {
                snackbarHostState.showSnackbar(
                    message = request.message,
                    actionLabel = request.actionLabel,
                    withDismissAction = true,
                    duration = SnackbarDuration.Indefinite,
                )
            }
    
            launch {
                delay(SNACKBAR_AUTO_DISMISS_MS)
                job.cancel()          // 3μ΄ˆκ°€ μ§€λ‚˜λ©΄ μž‘μ—… μ·¨μ†Œ
            }
        }
    }

    음 .. μž˜λ˜λŠ”κ±° κ°™μ£ ..??

    κ·Έλž˜μ„œ ! 이제 ν•΄κ²°λœκ±° μ•„λ‹ˆλƒ 라고 μƒκ°ν•˜μ‹€ 수 도 μžˆμ§€λ§Œ ν•΄λ‹Ή κ΅¬μ‘°λŠ” λ¬Έμ œμ μ„ ν•˜λ‚˜ κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€.

    이전에 dismiss 된 코루틴 즉, μ΄μ „μ˜ delay κ°€ 계속 λ™μž‘ν•˜κ³  μžˆκΈ°μ— λ¬Έμ œκ°€ λΌμš”.
    μ œκ°€ μ›ν•œκ±΄ μŠ€λ‚΅λ°”κ°€ μƒˆλ‘œ 호좜이되면 μ΄μ „μ˜ μž‘μ—…μ΄ μ™„μ „νžˆ μ·¨μ†Œ 되기λ₯Ό λ°”λž¬μ–΄μš”.

    λ”°λΌμ„œ 이전 job 을 μ°Έμ‘°ν•˜μ—¬ μƒˆλ‘œμš΄ μž‘μ—…μ΄ λ“€μ–΄μ˜€λ©΄ cancle ν•΄μ£ΌλŠ” λ‘œμ§μ„ κ΅¬μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

  • μ΅œμ’… μ½”λ“œ

    private const val SNACKBAR_AUTO_DISMISS_MS = 3000L
    
    class MelonSnackbarController(
        private val coroutineScope: CoroutineScope,
    ) {
        val snackbarHostState = SnackbarHostState()
    
        var currentRequest by mutableStateOf<MelonSnackbarActionRequest?>(null)
            private set
    
        private var actionJob: Job? = null // 이전 job 을 μ°Έμ‘°ν•  λ³€μˆ˜
        private var timerJob: Job? = null  // 이전 job 을 μ°Έμ‘°ν•  λ³€μˆ˜
    
        fun show(request: MelonSnackbarActionRequest) {
            currentRequest = request
    
            coroutineScope.launch {
                clearCurrentSnackbar()
    
                val job =
                    launch {
                        snackbarHostState.showSnackbar(
                            message = request.message,
                            actionLabel = request.actionLabel,
                            withDismissAction = true,
                        )
                    }
    
                actionJob = job
    
                timerJob =
                    launch {
                        delay(SNACKBAR_AUTO_DISMISS_MS)
                        clearCurrentSnackbar()
                        currentRequest = null
                    }
            }
        }
    
            // 전체 job cancel 및 λ¦¬μ†ŒμŠ€ 제거
        private fun clearCurrentSnackbar() {
            actionJob?.cancel()
            timerJob?.cancel()
            actionJob = null
            timerJob = null
        }
    
        fun performAction() {
            currentRequest?.onClick?.invoke()
            currentRequest = null
    
            clearCurrentSnackbar()
        }
    }        

4. [μΆ”ν›„ λͺ©ν‘œ] 더 κ°œμ„ ν•˜κΈ° μœ„ν•΄ μ–΄λ–€ 점을 더 κ³ λ €ν•  수 μžˆλŠ”μ§€

μ•žμ„œ κ΅¬ν˜„ν•œ 방식은 Job 을 직접 λ“€κ³  μžˆμœΌλ©΄μ„œ
이전 μŠ€λ‚΅λ°” Job/타이머λ₯Ό μ·¨μ†Œν•˜κ³  μƒˆ μŠ€λ‚΅λ°”λ₯Ό λ„μš°λŠ” λ°©μ‹μ΄μ—ˆλŠ”λ°μš” !

좔후에 ν•΄λ‹Ή μ½”λ“œλ₯Ό κ°œμ„ ν•œλ‹€κ³  ν•œλ‹€λ©΄ Flow + collectLatest 둜 flow 기반의 처리둜 μš”μ²­μ‹œλ§ˆλ‹€ 맀번 λ§ˆμ§€λ§‰ μš”μ²­λ§Œμ„ μ²˜λ¦¬ν•΄μ„œ κ΅¬ν˜„ν•  μˆ˜λ„ μžˆμ„ κ±° κ°™μŠ΅λ‹ˆλ‹€ !

또, ν˜„μž¬ κ΅¬μ‘°λŠ” 맀번 μŠ€λ‚΅λ°”λ₯Ό μƒˆλ‘œ λΆˆλŸ¬μ˜€λŠ”λ° ν•΄λ‹Ή 방식 말고 μŠ€λ‚΅λ°”κ°€ λ– μžˆλŠ” λ™μ•ˆμ—λŠ” μƒˆλ‘œμš΄ μŠ€λ‚΅λ°”κ°€ μ•ˆ 뜨게 ν•˜λΌλŠ” μš”κ΅¬μ‚¬ν•­μ΄ μžˆμ„ μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

κ·ΈλŸ°κ²½μš°μ—λŠ” Channel 의 BufferOverFlow 정책을 μ μš©ν•΄ μ‚¬μš©ν•˜κ±°λ‚˜ μ΄μ „μ˜ μš”μ²­ 값을 비ꡐ해 이λ₯Ό λ§‰λŠ” 방식이 μžˆκ² μŠ΅λ‹ˆλ‹€ γ…Žγ…Ž..

κΈ€ 읽어 μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€ ~!!



πŸ“•Β μ°Έκ³  자료

λ‹€μŒμ˜ 링크λ₯Ό μ°Έκ³ ν–ˆμŠ΅λ‹ˆλ‹€.

compose snackbar 곡식 λ¬Έμ„œ
compose custom snackbar 아티클

profile
Seungjun Gong

0개의 λŒ“κΈ€