본문 바로가기

안드로이드/Compose Dialog

[안드로이드 Jetpack Compose Dialog 시리즈] Checkbox Dialog 구현하기 #7


안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 Checkbox를 보여주는 Dialog를 만들겠습니다. Compose의 기본 작동방식에 대해 더 초점이 맞춰져 있기 때문에 체크박스를 어떻게 만들고, 각 체크 상태를 어떤 식으로 처리해야 하는지를 중점적으로 설명하겠습니다.

안드로이드 compose을 이용해서 만든 checkbox dialog
안드로이드 compose을 이용해서 만든 checkbox dialog

1. Checkbox Dialog 만들기

Compose Material3에서 제공하는 Checkbox를 사용해서 Dialog를 만들 예정입니다. 웹 개발을 해보신 분들이라면 Checkbox에는 텍스트를 표시하는 label도 필요하다고 생각할 수 있지만, 안드로이드 Compose에서는 Row를 이용해서 Checkbox와 Text를 배치합니다.

아래 코드를 보면 어떤 식으로 label을 표현하는지 확인할 수 있습니다.

또한, 터치를 했을 때 Row의 clickable를 활용해서 각 상태를 변경하도록 구현했습니다. 왜냐하면 체크박스 UI와 Text UI 둘 중 어느 곳을 터치해도 체크박스의 상태가 반영되기를 원했기 때문입니다.

// CustomCheckboxDialog.kt
@Composable
fun CustomCheckboxDialog(
    initialCheckboxList: ArrayList<CheckboxState>?,
    onClickCancel: () -> Unit,
    onClickConfirm: (ArrayList<CheckboxState>) -> Unit
) {
    val checkboxList = initialCheckboxList?.map {
        it.copy(
            text = it.text,
            isChecked = mutableStateOf(it.isChecked.value)
        )
    }?.let { ArrayList(it) } ?: ArrayList()

    Dialog(
        onDismissRequest = { onClickCancel() },
    ) {
        Card(
            shape = RoundedCornerShape(8.dp), // Card의 모든 꼭지점에 8.dp의 둥근 모서리 적용
        ) {
            Column(
                modifier = Modifier
                    .width(300.dp)
                    .wrapContentHeight()
                    .background(
                        color = Color.White,
                    )
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {

                Text(text = "추가 옵션을 선택해주세요.")

                Spacer(modifier = Modifier.height(15.dp))

                // checkbox list
                for (checkboxState in checkboxList) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable {
                                checkboxState.isChecked.value = !checkboxState.isChecked.value
                            },
                        verticalAlignment = Alignment.CenterVertically,

                        ) {
                        Checkbox(
                            checked = checkboxState.isChecked.value,
                            onCheckedChange = null
                        )

                        Spacer(modifier = Modifier.width(5.dp))

                        Text(
                            text = checkboxState.text,
                            modifier = Modifier
                                .fillMaxWidth(0.8f)
                        )
                    }

                    Spacer(modifier = Modifier.height(5.dp))

                }

                Spacer(modifier = Modifier.height(15.dp))

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center,
                ) {
                    Button(onClick = {
                        onClickCancel()
                    }) {
                        Text(text = "취소")
                    }

                    Spacer(modifier = Modifier.width(5.dp))

                    Button(onClick = {
                        onClickConfirm(checkboxList)
                    }) {
                        Text(text = "확인")
                    }
                }
            }
        }
    }
}

2. Checkbox Dialog 사용하기

CheckBox Dialog를 편하게 사용하기 위해서 상태홀더 클래스를 만들었습니다. Dialog에 표시할 CheckboxList 및 Dialog 표시 여부 등 Dialog에 관련된 정보를 저장합니다. CheckboxState의 text는 Checkbox의 label 역할을 하기 위해서 만들었습니다.

// CustomCheckboxDialogState.kt
data class CustomCheckboxDialogState(
    var checkboxList: ArrayList<CheckboxState>? = null,
    var isShowDialog: Boolean = false,
    val onClickConfirm: (ArrayList<CheckboxState>) -> Unit,
    val onClickCancel: () -> Unit,
)

data class CheckboxState(
    val text: String,
    var isChecked: MutableState<Boolean> = mutableStateOf(false),
)


MainViewModel에서는 상태홀더 클래스를 생성하고 초기값을 저장합니다. CheckboxState의 isChecked는 mutableStateOf 타입으로 지정해서 값이 변할 때마다 렌더링을 새로 그려줍니다. 그래서 사용자가 터치했을 때 체크표시가 바뀌게 됩니다.

// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {
   
    val customCheckboxDialogState: MutableState<CustomCheckboxDialogState?> =
        mutableStateOf(null)

    init {
        customCheckboxDialogState.value = CustomCheckboxDialogState(
            checkboxList = arrayListOf(
                CheckboxState("치킨무", mutableStateOf(false)),
                CheckboxState("어니언 양파링", mutableStateOf(false)),
                CheckboxState("치즈스틱", mutableStateOf(false)),
                CheckboxState("떡볶이", mutableStateOf(false)),
                CheckboxState("소떡소떡", mutableStateOf(false)),
            ),
            onClickConfirm = { checkboxList ->
                customCheckboxDialogState.value = customCheckboxDialogState.value?.copy(
                    isShowDialog = false,
                    checkboxList = checkboxList
                )
            },
            onClickCancel = {
                customCheckboxDialogState.value = customCheckboxDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showCheckboxDialog() {
        customCheckboxDialogState.value =
            customCheckboxDialogState.value?.copy(isShowDialog = true)
    }
}


MainScreen에서는 MainViewModel에서 만든 상태홀더 클래스를 통해서 Dialog를 보여주는 기능을 구현했습니다. 버튼을 누르면 viewModel.showCheckboxDialog()를 호출해서 상태홀더 클래스의 isShowDialog를 true로 변경해 주고, 이를 Composable 함수에서 감지하여 위에서 구현한 CustomCheckboxDialog가 팝업창으로 나오게 됩니다.

// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {
    val customCheckboxDialogState = viewModel.customCheckboxDialogState.value

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = "메인화면") },
                navigationIcon = {
                    IconButton(onClick = { mainNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues ->
        Column(
            modifier = Modifier.padding(paddingValues)
        ) {
            Button(
                onClick = { viewModel.showCheckboxDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = "7. Show CheckboxDialog",
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = "text: ${viewModel.customCheckboxDialogState.value?.checkboxList?.map { it.isChecked.value }?.toString() ?: "체크를 해보세요."}",
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customCheckboxDialogState?.isShowDialog == true) {
                CustomCheckboxDialog(
                    initialCheckboxList = customCheckboxDialogState.checkboxList,
                    onClickCancel = customCheckboxDialogState.onClickCancel,
                    onClickConfirm = customCheckboxDialogState.onClickConfirm
                )
            }
        }
    }
}

3. Checkbox Dialog 정리

이번 포스팅에서는 안드로이드 Jetpack Compose의 Material3에서 Checkbox를 활용하여 Checkbox Dialog를 만들었습니다. 오늘 예제로 구현한 것처럼 Compose의 Material3을 사용하면 xml에서 복잡하게 구현했던 UI를 쉽고 편리하게 구현할 수 있는 장점이 있습니다.

다만, 선언형 UI 프로그래밍은 상태 관리를 통해서 렌더링, 리렌더링을 진행하기 때문에 상태 관리에 대한 이해가 중요하고, 무분별하게 상태를 선언하면 그만큼 렌더링의 최적화에 문제가 생기기 때문에 조심해서 다뤄야 합니다.

다음 포스팅에서는 RadioButton을 사용한 Dialog를 구현하겠습니다.

반응형