본문 바로가기

안드로이드/Compose Dialog

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


안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 Text를 입력할 수 있는 BasicTextField를 사용해서 Dialog를 구현하겠습니다. Compose에서는 TextField와 BasicTextField가 있는데 디자인을 커스텀하기 위해서는 BasicTextField를 사용하는 것이 유리하기 때문에 BasicTextField로 Dialog를 만들겠습니다.

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

1. BasicTextField Dialog 만들기

Compose Material3에서 제공하는 Dialog와 BasicTextField를 사용해서 TextFieldDialog를 만들었습니다. text를 표시하기 위해서 TextState를 관리하는 변수를 만들고, 해당 변수를 BasicTextField의 value와 onValueChange에서 활용해야 합니다. 이렇게 해야 사용자가 입력한 텍스트가 실시간으로 UI에 표시되기 때문입니다.

아래 코드를 보면 BasicTextField에서 decorationBox를 재정의하는 것을 확인할 수 있습니다. decorationBox를 통해서 TextField의 디자인을 원하는 형태로 수정할 수 있기 때문입니다.

// CustomTextFieldDialog.kt
@Composable
fun CustomTextFieldDialog(
    initialText: String?,
    onClickCancel: () -> Unit,
    onClickConfirm: (text: String) -> Unit
) {

    val text = remember { mutableStateOf(initialText ?: "") }

    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))

                // TextField
                BasicTextField(
                    value = text.value,
                    onValueChange = { text.value = it },
                    singleLine = true,
                    textStyle = TextStyle(
                        color = Color.Black,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Normal,
                    ),
                    decorationBox = { innerTextField ->
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .background(
                                    color = Color(0xFFBAE5F5),
                                    shape = RoundedCornerShape(size = 16.dp)
                                )
                                .padding(all = 16.dp),
                            verticalAlignment = Alignment.CenterVertically,
                        ) {
                            Icon(
                                imageVector = Icons.Default.Create,
                                contentDescription = "",
                                tint = Color.DarkGray,
                            )
                            Spacer(modifier = Modifier.width(width = 8.dp))
                            innerTextField()
                        }
                    },
                )

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

2. BasicTextField Dialog 사용하기

위에서 만든 BasicTextFieldDialog를 사용하기 위해서 상태를 관리하는 클래스를 만든 후, Screen과 ViewModel에서 비즈니스 로직과 UI 영역을 만들었습니다.

CustomTextFieldDialogState에서는 사용자가 입력한 text를 저장하고 있으며, Dialog에 관련된 기능을 모두 포함하게 구현했습니다.

// CustomTextFieldDialogState.kt
data class CustomTextFieldDialogState(
    var text: String? = null,
    var isShowDialog: Boolean = false,
    val onClickConfirm: (text: String) -> Unit,
    val onClickCancel: () -> Unit,
)


아래 코드는 ViewModel입니다. 위에서 만든 상태 관련 클래스를 MutableState에 저장하고 있으며, Dialog에서 확인을 눌렀을 때 사용자가 입력한 Text를 저장하기 위해서 onClickConfirm 및 onClickCancel 함수에서 text와 isShowDialog에 대한 처리를 하고 있습니다. 

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

    val customTextFieldDialogState: MutableState<CustomTextFieldDialogState?> =
        mutableStateOf(null)

    init {
        customTextFieldDialogState.value = CustomTextFieldDialogState(
            onClickConfirm = { text ->
                customTextFieldDialogState.value = customTextFieldDialogState.value?.copy(
                    isShowDialog = false,
                    text = text
                )
            },
            onClickCancel = {
                customTextFieldDialogState.value = customTextFieldDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showTextFieldDialog() {
        customTextFieldDialogState.value =
            customTextFieldDialogState.value?.copy(isShowDialog = true)
    }
}


UI를 보여주는 MainScreen 코드입니다. isShowDialog 변수를 통해서 BasicTextFieldDialog 노출 여부를 결정하고 있으며, 사용자가 입력한 텍스트를 버튼 아래에 배치하여 보여주고 있습니다. 사용하려는 목적에 맞게 수정해서 활용하면 좋을 것 같습니다.

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

    val customTextFieldDialogState = viewModel.customTextFieldDialogState.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.showTextFieldDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = "6. Show TextFieldDialog",
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = "text: ${viewModel.customTextFieldDialogState.value?.text ?: "텍스트를 입력해주세요."}",
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customTextFieldDialogState?.isShowDialog == true) {
                CustomTextFieldDialog(
                    initialText = customTextFieldDialogState.text,
                    onClickCancel = customTextFieldDialogState.onClickCancel,
                    onClickConfirm = customTextFieldDialogState.onClickConfirm
                )
            }
        }
    }
}

3. BasicTextField Dialog 정리

이번 포스팅에서는 안드로이드 Jetpack Compose에서 BasicTextFieldDialog를 구현하고 사용하는 방법에 대해서 알아봤습니다. 구현하는데 어려움은 없었지만 실제로 개발할 때는 TextField의 디자인을 커스텀해야 하는 경우가 많습니다. 그래서 BasicTextField를 사용했으며, decorationBox를 재정의해서 TextField의 디자인을 변경할 수 있었습니다.

이번 과정을 통해서 여러분이 제작하고자 하는 Dialgo와 TextField를 커스텀하는데 도움이 됐으면 좋겠습니다.

반응형