본문 바로가기

안드로이드/Compose Dialog

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


안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose의 RadioButton을 사용해서 RadioButton Dialog를 구현하겠습니다. RadioButton의 특징은 다중 선택이 안되고, 한 가지만 선택할 수 있는 UI입니다. 그래서 일반적으로는 RadioGroup 등을 이용해서 RadioButton의 그룹을 만든 후, 한 가지만 선택되게 구현해 왔습니다. 그런데 Compose의 Material3 RadioButton에서는 RadioGroup은 없습니다. 어떤 형태로 구현할 수 있는지 예제를 통해 확인하겠습니다.

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

1. RadioButton Dialog 만들기

안드로이드 Compose의 Material3에서 제공하는 RadioButton을 활용해서 만든 RadioButon Dialog입니다. 반복문을 이용해서 RadioButton을 보여주고 있고, RadioButton과 Text를 감싸고 있는 Row에 selectable()을 활용해서 클릭 됐을 때 모든 라디오 버튼의 체크를 해제하고, 현재 클릭한 라디오 버튼의 체크표시를 하고 있습니다.

RadioButton의 Label은 Text가 대체하고 있습니다.

클릭했을 때 체크 표시가 정상적으로 반영되기 위해서 mutableStateOf<Boolean>으로 선언된 isChecked 변수를 바인딩 한 모습을 볼 수 있습니다.

// CustomRadioButtonDialog.kt
@Composable
fun CustomRadioButtonDialog(
    initialRadioButtonList: ArrayList<RadioButtonState>?,
    onClickCancel: () -> Unit,
    onClickConfirm: (ArrayList<RadioButtonState>) -> Unit
) {
    val radioButtonList = initialRadioButtonList?.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))

                // radioButton list
                for (radioButtonState in radioButtonList) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .selectable(
                                selected = radioButtonState.isChecked.value,
                                onClick = {
                                    radioButtonList.forEach {
                                        it.isChecked.value = false
                                    }
                                    radioButtonState.isChecked.value = true
                                }
                            ),
                        verticalAlignment = Alignment.CenterVertically,

                        ) {
                        RadioButton(
                            selected = radioButtonState.isChecked.value,
                            onClick = null
                        )

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

                        Text(
                            text = radioButtonState.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(radioButtonList)
                    }) {
                        Text(text = "확인")
                    }
                }
            }
        }
    }
}

2. RadioButton Dialog 사용하기

RadioButton Dialog를 사용하기 위해서 상태홀더 클래스를 정의했습니다. 해당 클래스에는 Dialog에 표시될 라디오 버튼들의 배열을 저장하고 있으며, 팝업창의 노출 여부 등의 데이터를 관리하고 있습니다. 위의 코드에서 보았듯이 isChecked는 MutableState<Boolean> 타입으로 지정해서 상태가 변할 때마다 렌더링이 다시 되게 구현했습니다.

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

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


MainViewModel에서는 상태홀더 클래스를 생성하고 저장합니다. UI를 담당하는 영역에서 해당 ViewModel의 상태홀더 클래스를 가져와서 팝업창의 노출 여부 및 현재 선택된 라디오 버튼 정보를 보여줄 예정입니다. showRadioButtonDialog() 함수는 MainScreen에서 버튼을 눌렀을 때 실행 될 함수입니다.

// MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle?,
) : ViewModel() {

    val customRadioButtonDialogState: MutableState<CustomRadioButtonDialogState?> =
        mutableStateOf(null)

    init {
        customRadioButtonDialogState.value = CustomRadioButtonDialogState(
            radioButtonList = arrayListOf(
                RadioButtonState("아주좋음", mutableStateOf(false)),
                RadioButtonState("좋음", mutableStateOf(false)),
                RadioButtonState("보통", mutableStateOf(false)),
                RadioButtonState("약간", mutableStateOf(false)),
                RadioButtonState("아주약간", mutableStateOf(false)),
            ),
            onClickConfirm = { radioButtonList ->
                customRadioButtonDialogState.value = customRadioButtonDialogState.value?.copy(
                    isShowDialog = false,
                    radioButtonList = radioButtonList
                )
            },
            onClickCancel = {
                customRadioButtonDialogState.value = customRadioButtonDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showRadioButtonDialog() {
        customRadioButtonDialogState.value =
            customRadioButtonDialogState.value?.copy(isShowDialog = true)
    }
}


MainScreen에서는 ViewModel에서 생성한 상태홀더 클래스를 가져와서 각 상태에 맞게 UI를 보여주게 구현했습니다. 버튼을 누르면 isShowDialog가 true가 돼서 팝업창이 보이고, 팝업창에서 확인을 누르면 RadioButtonList의 isChecked 변수가 변하게 돼서, 사용자가 선택한 옵션을 전달받을 수 있게 구현했습니다.

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

    val customRadioButtonDialogState = viewModel.customRadioButtonDialogState.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.showRadioButtonDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = "8. Show RadioButtonDialog",
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = "text: ${viewModel.customRadioButtonDialogState.value?.radioButtonList?.map { it.isChecked.value }?.toString() ?: "체크를 해보세요."}",
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customRadioButtonDialogState?.isShowDialog == true) {
                CustomRadioButtonDialog(
                    initialRadioButtonList = customRadioButtonDialogState.radioButtonList,
                    onClickCancel = customRadioButtonDialogState.onClickCancel,
                    onClickConfirm = customRadioButtonDialogState.onClickConfirm
                )
            }
        }
    }
}

 

3. RadioButton Dialog 정리

이번 포스팅에서는 안드로이드 Jetpack Compose의 Material3에서 RadioButton을 활용하여 RadioButton Dialog를 구현하는 과정에 대해 알아봤습니다. Dialog와 관련된 모든 데이터를 하나의 클래스에서 관리할 수 있도록 상태홀더 클래스를 구현하여 캡슐화를 진행했습니다. 이렇게 여러 가지 데이터를 하나의 변수로 선언하는 것보다 상태홀더 클래스 같은 패턴으로 데이터를 관리하면 책임이 분리되기 때문에 추후 유지보수 측면에서 유리하고 가독성 또한 좋아집니다.

이번 시간에 알아본 내용을 토대로 여러분만의 RadioButton과 Dialog를 만드는데 도움이 됐으면 좋겠습니다. 다음 포스팅에서는 ViewPager, TabPager에 대해 포스팅할 예정입니다.

반응형