본문 바로가기
안드로이드 공부 노트/Compose(컴포즈)

[Android-Compose] 안드로이드 컴포즈 바텀시트 구현해보기! (Android Compose BottomSheet) - ModalBottomSheetLayout, ModalBottomSheet

by 지게요 2025. 4. 20.
728x90
반응형

이번 공부 노트는 안드로이드 컴포즈에서 바텀 시트를 예제를 통해 구현해 보도록 하겠다.

구현하는 방법은 ModalBottomSheetLayout, ModalBottomSheet 크게 두 가지가 있다.

하나하나씩 알아보자! 

※ 본 예제는 MaterialDesign3 기준으로 작성했다.

해당 버튼을 누르면 과일 리스트가 바텀시트로 나오는 예제이다.

# ModalBottomSheetLayout

이 방식은 구버전 API로 콘텐츠와 바텀 시트를 함께 포함하는 레이아웃 컴포넌트이다.

일반적으로 화면 전체를 감싸는 방식으로 사용

따라서 콘텐츠와 바텀 시트를 함께 포함하고 싶다면 사용 추천!

 

ModalBottomSheetLayout 내부를 살펴보자

@Composable
@ExperimentalMaterialApi
fun ModalBottomSheetLayout(
    sheetContent: @Composable ColumnScope.() -> Unit,
    modifier: Modifier = Modifier,
    sheetState: ModalBottomSheetState =
        rememberModalBottomSheetState(Hidden),
    sheetShape: Shape = MaterialTheme.shapes.large,
    sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
    sheetBackgroundColor: Color = MaterialTheme.colors.surface,
    sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
    scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
    content: @Composable () -> Unit
)

 

각 각 매개변수가 어떤 것인지 중요한 것만 간략하게 아래 정리 해보았다.

sheetContent 바텀 시트 내부에 표시될 콘텐츠를 정의하는 컴포저블 람다 함수
sheetState 바텀 시트의 상태(열림/닫힘)를 관리하는 객체 기본은 닫힘(Hidden)
content 바텀 시트 이외의 메인 화면 콘텐츠를 정의하는 컴포저블 함수

 

어떤 것인지 알아봤다면 이제 구현해 보자!

## 1. rememberModalBottomSheetState 생성

// sheetState를 위해 rememberModalBottomSheetState 생성 (기본으로 닫힘 상태)
val bottomSheetState = rememberModalBottomSheetState(
    initialValue = ModalBottomSheetValue.Hidden,
)

## 2. rememberCoroutineScope 추가

// show, hide 사용을 위해 추가
val coroutineScope = rememberCoroutineScope()

코루틴 스코프를 추가해 주는 이유는 바텀시트를 보여주고 숨기는 함수가 suspend이기 때문에 코루틴 스코프를 만들어준다.

보여주고 숨기는 함수는 위에서 만들어준 bottomSheetState안에 있다. (아래 참고)

 

<보여주는 함수>

coroutineScope.launch {
    bottomSheetState.show()
}

 

<숨기는 함수>

coroutineScope.launch {
    bottomSheetState.hide()
}

## 3. ModalBottomSheetLayout 추가

ModalBottomSheetLayout(
    sheetState = bottomSheetState,
    sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
    sheetContent = {
        // 바텀 시트 내부 콘텐츠
        val fruitList = listOf("사과", "바나나", "포도", "오렌지", "토마토")
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 15.dp, horizontal = 15.dp)
        ) {
            fruitList.forEach { fruitName ->
                Text(
                    modifier = Modifier.fillMaxWidth(),
                    text = fruitName,
                    fontSize = 14.sp
                )
            }
        }
    },
    content = {
        // 메인 콘텐츠
        Box(modifier = Modifier.fillMaxSize()){
            Button(onClick = {
                coroutineScope.launch {
                    bottomSheetState.show()
                }
            }) {
                Text(
                    text = "과일 리스트 바텀 시트"
                )
            }
        }
    }
)

 

이렇게 각 선언한 변수와 ModalBottomSheetLayout 매개변수에 맞게 넣고 구현해 주면 끝이 난다!

 

# ModalBottomSheet

이 방법은 Material3에서 새롭게 나온 API이다.

그래서 그런지 훨씬 사용하기 편하다.

ModalBottomSheetLayout와의 차이점이라면 ModalBottomSheet은 이름에서 볼 수 있다시피 Layout이 빠졌다.

그 의미는 콘텐츠와 바텀 시트를 함께 포함하지 않아도 된다는 의미이다.

구 버전 API를 사용하면 무조건 함께 포함해야 해서 기존 화면에 바텀 시트 추가를 하려면 번거로웠던 문제를 해결해 준 셈이다.

 

ModalBottomSheet 내부 코드이다.

@Composable
@ExperimentalMaterial3Api
fun ModalBottomSheet(
    onDismissRequest: () -> Unit,
    modifier: Modifier = Modifier,
    sheetState: SheetState = rememberModalBottomSheetState(),
    sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
    shape: Shape = BottomSheetDefaults.ExpandedShape,
    containerColor: Color = BottomSheetDefaults.ContainerColor,
    contentColor: Color = contentColorFor(containerColor),
    tonalElevation: Dp = BottomSheetDefaults.Elevation,
    scrimColor: Color = BottomSheetDefaults.ScrimColor,
    dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
    windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
    properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(),
    content: @Composable ColumnScope.() -> Unit,
) {

 

대부분 ModalBottomSheetLayout와 매개변수들은 똑같다. 하지만 자세히 보면 몇몇 개의 차이점이 있다.

sheetMaxWidth 바텀 시트의 최대 너비를 제한
dragHandle 바텀 시트 상단에 드래그 핸들 UI 표시

 

구현은 ModalBottomSheetLayout와 비슷하지만 살짝 다르다.

## 레이아웃 표시를 위한 remember state 변수 생성

ModalBottomSheet는 state를 통해 제어하지 않고 따로 변수를 만들어서 표시 유무를 컨트롤해 준다.

// 레이아웃 표시를 위한 변수
var isShowBottomSheet by remember{ mutableStateOf(false) }

 

## ModalBottomSheet 추가

메인 콘텐츠는 그대로 놔두고 Box와 ModalBottomSheet를 추가해 준다.

  // 레이아웃 표시를 위한 변수
    var isShowBottomSheet by remember{ mutableStateOf(false) }

    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        // 메인 콘텐츠
        Button(
            modifier = Modifier.align(Alignment.TopCenter),
            onClick = {
                isShowBottomSheet = !isShowBottomSheet
            }) {
            Text(
                text = "과일 리스트 바텀 시트"
            )
        }
    }

    if (isShowBottomSheet){
        ModalBottomSheet(
            onDismissRequest = {
                isShowBottomSheet = false
            },
            content = {
                // 바텀 시트 내부 콘텐츠
                val fruitList = listOf("사과", "바나나", "포도", "오렌지", "토마토")
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 15.dp, horizontal = 15.dp)
                ) {
                    fruitList.forEach { fruitName ->
                        Text(
                            modifier = Modifier.fillMaxWidth(),
                            text = fruitName,
                            fontSize = 14.sp
                        )
                    }
                }
            }
        )
    }
}

 

보이는 것과 같이 똑같이 실행이 된다.

 

❗️여기서 rememberModalBottomSheetState는 사용 안 함?이라고 물을 수 있다.

 

ModalBottomSheet에서 BottomSheetState는 아래 코드와 같이 활용해서 사용할 수 있다.

 val sheetState = rememberModalBottomSheetState(
        skipPartiallyExpanded = true,
    )

  // sheetState.currentValue == SheetValue.Hidden
  // sheetState.currentValue == SheetValue.Expanded
  // sheetState.currentValue == SheetValue.PartiallyExpanded

    // Check the current state
    when (sheetState.currentValue) {
        SheetValue.Hidden -> {
            Text("Sheet is Hidden")
        }
        SheetValue.Expanded -> {
            Text("Sheet is Expanded")
        }
        SheetValue.PartiallyExpanded -> {
            Text("Sheet is Partially Expanded")
        }
    }

 


처음부터 BottomSheet를 생각하는 게 아니라면 ModalBottomSheet를 사용하는 게 유지보수에 더 좋고 활용도가 더 좋을 거 같다는 생각이 들었다

 

 

참고
https://develop-oj.tistory.com/82
https://medium.com/@ramadan123sayed/bottom-sheets-in-jetpack-compose-modalbottomsheet-vs-bottomshee tscaffold-24751326 e0 ec
반응형