개발 공부/안드로이드

[Compose] 컴포즈 공부하기 7 - Snackbar

yong_DD 2023. 9. 11. 18:22

이전 scaffold에서 설명하지 않았던 Snackbar에 대해 알아보자!

 

Compose에서 Snackbar는 SnackbarHost를 통해 쉽게 사용할 수 있다.

(SnackbarHost를 통하지 않으면 Snackbar의 상태와 꺼지는 시간을 직접 구현해야 할 텐데 정말 필요할 경우가 아니면 굳이..?)

 

이 SnacbarHost는 Scaffold에서 제공하므로 Scaffold를 사용하면 쉽게 사용할 수 있다.

그러나 Scaffold를 사용하지 않을 수도 있기 때문에 Scaffold에서 사용했을 때와 아닐 때를 나눠서 설명을 하겠다.


우선 SnackbarHost에 대해 알아보자!

[Scaffold]에서 [Snackbar]를 사용하여 항목을 적절히 표시, 숨기기 및 취소할 호스트
기본 매개 변수가 포함된 이 구성 요소는 [Scaffold]에 내장되어 있습니다.
기본값으로 [Snackbar], 그리고 [ScaffoldState.snackbarHostState]]와 [스낵바 Host State.showSnackbar]를 사용한다.

SnackbarHost.kt를 봐보면 위와 같이 설명이 되어있다.

Scaffold를 사용할 때 쓸 수 있는 기능이라고 볼 수 있을 것 같다.

@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.duration.toMillis(
                currentSnackbarData.actionLabel != null,
                accessibilityManager
            )
            delay(duration)
            currentSnackbarData.dismiss()
        }
    }
    FadeInFadeOutWithScale(
        current = hostState.currentSnackbarData,
        modifier = modifier,
        content = snackbar
    )
}

내부 코드를 보면, SnackbarHostState, modifier, SnackbarData를 파라미터로 받고 있다.

그리고 SnackbarData에서 지정한 값에 따라 delay해주고 dismiss를 해주고 있다.

띄웠다가 시간이 지나면 꺼지는 것을 신경쓰지 않도록 편하게 구현이 되어있다!

 

그렇다면  SnacbarHostState 는 어떤 구조로 되어 있을까?

@Stable
class SnackbarHostState {

    /**
     * Only one [Snackbar] can be shown at a time.
     * Since a suspending Mutex is a fair queue, this manages our message queue
     * and we don't have to maintain one.
     */
    private val mutex = Mutex()

    /**
     * The current [SnackbarData] being shown by the [SnackbarHost], of `null` if none.
     */
    var currentSnackbarData by mutableStateOf<SnackbarData?>(null)
        private set

    /**
		중간에 긴 설명은 생략 .....  
     */
    suspend fun showSnackbar(
        message: String,
        actionLabel: String? = null,
        duration: SnackbarDuration = SnackbarDuration.Short
    ): SnackbarResult = mutex.withLock {
        try {
            return suspendCancellableCoroutine { continuation ->
                currentSnackbarData = SnackbarDataImpl(message, actionLabel, duration, continuation)
            }
        } finally {
            currentSnackbarData = null
        }
    }

    @Stable
    private class SnackbarDataImpl(
        override val message: String,
        override val actionLabel: String?,
        override val duration: SnackbarDuration,
        private val continuation: CancellableContinuation<SnackbarResult>
    ) : SnackbarData {

        override fun performAction() {
            if (continuation.isActive) continuation.resume(SnackbarResult.ActionPerformed)
        }

        override fun dismiss() {
            if (continuation.isActive) continuation.resume(SnackbarResult.Dismissed)
        }
    }
}

SnackbarData의 구현체인 SnackbarDatatImpl과 showSnackbar로 되어 있다.

SnackbarData는 아래 코드를 참고해주면 된다 😄

 

혹시 Mutex에 대해 알고 싶다면 여기

 

interface SnackbarData {
    val message: String
    val actionLabel: String?
    val duration: SnackbarDuration

    /**
     * Function to be called when Snackbar action has been performed to notify the listeners
     */
    fun performAction()

    /**
     * Function to be called when Snackbar is dismissed either by timeout or by the user
     */
    fun dismiss()
}

enum class SnackbarDuration {
    /**
     * Show the Snackbar for a short period of time
     */
    Short,

    /**
     * Show the Snackbar for a long period of time
     */
    Long,

    /**
     * Show the Snackbar indefinitely until explicitly dismissed or action is clicked
     */
    Indefinite
}

 

 

그렇다면 이제 실제 코드를 통해 어떻게 구현하는지를 알아보자!

 

1. Scaffold 사용

val cScope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()

Scaffold(scaffoldState=scaffoldState) {
    Button(onClick = {
        cScope.launch {
            val result =
                scaffoldState.snackbarHostState.showSnackbar(
                    "스낵바 😁😁😁",
                    "닫기",
                    SnackbarDuration.Short)

            when(result) {
                SnackbarResult.ActionPerformed -> {}
                SnackbarResult.Dismissed -> {}
            }
        }
    }){
        Text("스낵바 보이기")
    }
}

 

 

Scaffold에 remeberScaffoldState를 넣어주고, 

scaffoldState를 통해서 snackbarHostState를 통해 showSnackbar를 호출해 

버튼을 클릭하면 스택바가 보이도록 해주었다.

결과물은 왼쪽처럼 나오게 된다.

 

showSnackbar는 위의 SnackbarHostState의 코드를 보면 SnackbarResult를 반환하기 때문에 result를 받아서 동작에 따라 행동을 처리할 수가 있다.

 

그리고 showSnackbar를 호출할 때 coroutineScope를 사용해서 처리하는데 

showSnackbar 자체가 suspend fun이기 때문이다.

coroutineScope 내에서만 처리할 수 있다 😁

 

 

 

 

 

 

 

 

 

 

 

 

2. Scaffold 미사용

val cScope = rememberCoroutineScope()
val snackBarHost = remember { SnackbarHostState() }
Box(modifier = Modifier.fillMaxSize()) {
    Button(onClick = {
        cScope.launch {
            val snackBar =
                snackBarHost.showSnackbar(
                    "스낵바 😁😁😁",
                    "닫기",
                    SnackbarDuration.Short)

            // result
            when(snackBar) {
                SnackbarResult.ActionPerformed -> {}
                SnackbarResult.Dismissed -> {}
            }
        }
    }){
        Text("스낵바 보이기")
    }
    
    SnackbarHost(hostState = snackBarHost)
}

동일하게 SnackbarHostState를 사용하여 만들어주면 된다!

SnackbarHost를 안에 넣어주면 되는데 실행하게 되면 아래와 같이 나오게 된다.

 

 Box에 넣었기 때문에 위치가 저렇게 나오게 되고 Alignment를 사용하여 위치를 조정해서 사용할 수 있다 😄

ex) SnackbarHost( hostState = snackbarHostState, modifier = Modifier.align(Alignment.BottomCenter) )

 

 

 

당연히 Box만 가능한 것은 아니고, Column에 넣게 되면 하단과 같이 보이게 된다.

 

SnackBarHost를 Button보다 위에 올리게 되면?

버튼이 밀리면서 위에 뜨게 된다 !!!

 

 

 

++ 추가

Snackbar는 구현할 수 있는데, 이제 Toast는 어떻게 구현하면 좋을까?

@Composable
fun ToastGreetingButton(greeting: String) {
    val context = LocalContext.current
    Button(onClick = {
        Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show()
    }) {
        Text("Greet")
    }
}

CompositionLocal 클래스 (Compose에서 ContextConfiguration 또는 View 같은 Android 프레임워크 유형의 값을 전파하는 데 사용)를 사용하고 이 클래스는 Compose 코드가 해당 LocalContext, LocalConfiguration 또는 LocalView와 함께 호스팅된다.

current 속성을 사용해 현재 값에 액세스하게 된다.

LocalContext.current를 제공하여 토스트 메시지를 띄우게 된다!

 

자세한 것은 여길 참고!

 

 

 


참고 및 출처

 

Snackbar  |  Jetpack Compose  |  Android Developers

Snackbar Stay organized with collections Save and categorize content based on your preferences. The snackbar component serves as a brief notification that appears at the bottom of the screen. It provides feedback about an operation or action without interr

developer.android.com

 

 

7장 스레드 한정, 액터 그리고 뮤텍스 - Incheol's TECH BLOG

우리는 중요한 원칙인 정보 은닉을 실천에 옮겼다. 카운터의 구현을 숨겨서 향후에 뮤텍스, 원자적 변수, 또는 액터 없는 스레드 제한을 사용하도록 그것을 바꿀 수 있다.

incheol-jung.gitbook.io

 

 

[Coroutine] 5. suspend fun의 이해

일시중단 가능한 코루틴 코루틴은 기본적으로 일시중단 가능하다. launch로 실행하든 async로 실행하든 내부에 해당 코루틴을 일시중단 해야하는 동작이 있으면 코루틴은 일시 중단된다. 예시로

kotlinworld.com

 

Compose에서 뷰 사용  |  Jetpack Compose  |  Android Developers

Compose에서 뷰 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose UI에 Android 뷰 계층 구조를 포함할 수 있습니다. 이 접근 방식은 AdView와 같이 Compose에

developer.android.com