본문 바로가기

안드로이드/Compose Dialog

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


안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose에서 DatePicker Dialog를 구현하는 방법에 대해 포스팅하겠습니다. 직접 구현할 수도 있겠지만, 지금은 compose material3에서 기본으로 제공해주는 DatePickerDialog와 DatePicker 2가지를 이용해서 DatePicker Dialog를 구현하겠습니다.

안드로이드 compose material3을 이용해서 만든 date picker dialog
안드로이드 compose material3을 이용해서 만든 date picker dialog

1. DatePicker Dialog 만들기

먼저, compose material3에서 제공하는 DatePickerDialog와 DatePicker를 이용해서 UI를 만들겠습니다. datePickerState를 DatePicker에 전달해야하는데 UTC와 관련된 주의사항이 있습니다.

initialSelectedDateMillis에는 현재 선택된 날짜에 대한 밀리초 데이터를 전달해야하는데, 강제로 UTC 시간으로 적용됩니다. 즉, UTC를 고려하지않고 한국에서 밀리초로 변환된 날짜 데이터를 전달하면 -9시간이 적용된 결과가 DatePicker에 반영되기 때문에 원하는 결과가 나오지 않을 수 있습니다.

그래서 반드시 Timezone을 세팅해주어야 합니다.

// CustomDatePickerDialog.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomDatePickerDialog(
    selectedDate: String?,
    onClickCancel: () -> Unit,
    onClickConfirm: (yyyyMMdd: String) -> Unit
) {
    DatePickerDialog(
        onDismissRequest = { onClickCancel() },
        confirmButton = {},
        colors = DatePickerDefaults.colors(
            containerColor = Color.White
        ),
        shape = RoundedCornerShape(6.dp)
    ) {
        val datePickerState = rememberDatePickerState(
            yearRange = 2023..2024,
            initialDisplayMode = DisplayMode.Picker,
            initialSelectedDateMillis = selectedDate?.let {
                val formatter = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).apply {
                    // initialSelectedDateMillis는 UTC 시간을 받고 있다.
                    // 아래처럼 timeZone을 추가해서 UTC 시간으로 설정해야한다.
                    // 한국에서 실행한다면 -9시간을 적용하기 때문이다.
                    //
                    // 만약에 아래 코드가 없다면 20240228을 넘겼을 때, 안드로이드는 KST 20240228000000 으로 인식할 것이고,
                    // 이를 UTC 시간으로 변환하면서 -9시간을 적용하기 때문에 결과적으로 20240228을 넘기면 2024년 2월 27일에 선택이 되있는 문제가 발생한다.
                    timeZone = TimeZone.getTimeZone("UTC")
                }
                formatter.parse(it)?.time
                    ?: System.currentTimeMillis() // 날짜 파싱 실패 시 현재 시간을 기본값으로 사용
            } ?: System.currentTimeMillis(), // selectedDate가 null인 경우 현재 시간을 기본값으로 사용,
            selectableDates = object : SelectableDates {
                override fun isSelectableDate(utcTimeMillis: Long): Boolean {
                    // 날짜 제한 조건
                    return true
//                    return utcTimeMillis > System.currentTimeMillis()
                }
            })
        
        DatePicker(
            state = datePickerState,
        )
        
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center,
        ) {
            Button(onClick = {
                onClickCancel()
            }) {
                Text(text = "취소")
            }

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

            Button(onClick = {
                datePickerState.selectedDateMillis?.let { selectedDateMillis ->
                    val yyyyMMdd = SimpleDateFormat(
                        "yyyyMMdd",
                        Locale.getDefault()
                    ).format(Date(selectedDateMillis))

                    onClickConfirm(yyyyMMdd)
                }
            }) {
                Text(text = "확인")
            }
        }
    }
}

2. DatePicker Dialog 사용하기

DatePickerDialog를 사용하기 위해서 CustomDatePickerDialogState를 만들었습니다. 이 클래스는 선택된 날짜, dialog 표시여부, 클릭 이벤트, 취소 이벤트에 대한 처리를 담당하고 있습니다.

// CustomDatePickerDialogState.kt
data class CustomDatePickerDialogState(
    var selectedDate: String? = null,
    var isShowDialog: Boolean = false,
    val onClickConfirm: (yyyyMMdd: String) -> Unit = {},
    val onClickCancel: () -> Unit = {},
)


아래 코드는 ViewModel 입니다. 위에서 만들어준 클래스를 사용해서 상태관리를 해주고 있습니다. 사용자가 확인 버튼을 누른다면 isShowDialog를 false로 수정하고 selectedDate에 콜백함수로부터 전달받은 yyyyMMdd 형태의 값을 저장하고 있습니다.

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

    init {
        customDatePickerDialogState.value = CustomDatePickerDialogState(
            onClickConfirm = { yyyyMMdd ->
                customDatePickerDialogState.value = customDatePickerDialogState.value?.copy(
                    isShowDialog = false,
                    selectedDate = yyyyMMdd
                )
            },
            onClickCancel = {
                customDatePickerDialogState.value = customDatePickerDialogState.value?.copy(
                    isShowDialog = false
                )
            }
        )
    }

    fun showDatePickerDialog() {
        customDatePickerDialogState.value =
            customDatePickerDialogState.value?.copy(isShowDialog = true)
    }
}


아래는 UI를 보여주는 Screen 영역입니다. 버튼을 누르면 viewModel.showDatePickerDialog()를 호출해서 DatePickerDialog를 보여줍니다. 사용자가 날짜를 선택하고 확인 버튼을 누르면 dialog가 사라지고 Text 영역에 현재 선택한 날짜를 표시해줍니다.

// MainScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
    mainNavController: NavHostController,
    viewModel: MainViewModel
) {
    val customDatePickerDialogState = viewModel.customDatePickerDialogState.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.showDatePickerDialog() },
                shape = RectangleShape,
            ) {
                Text(
                    text = "4. Show DatePickerDialog",
                    textAlign = TextAlign.Center,
                    style = TextStyle(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold
                    )
                )
            }
            Text(
                text = "Selected Date: ${viewModel.customDatePickerDialogState.value?.selectedDate ?: "날짜를 선택해주세요."}",
                style = TextStyle(
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Bold
                )
            )

            if (customDatePickerDialogState?.isShowDialog == true) {
                CustomDatePickerDialog(
                    selectedDate = customDatePickerDialogState.selectedDate,
                    onClickCancel = customDatePickerDialogState.onClickCancel,
                    onClickConfirm = customDatePickerDialogState.onClickConfirm
                )
            }
        }
    }
}

3. DatePicker Dialog 정리

안드로이드 Jetpack Compose Material3를 사용하여 DatePickerDialog를 구현하는 것은 사용자에게 효과적인 날짜 선택 인터페이스를 제공하는 좋은 방법인 것 같습니다. UTC 시간대 처리와 같은 몇 가지 주의사항을 고려해야 하지만, 위에서 설명한 내용을 이해했다면 문제 없이 구현할 수 있습니다.

이번 포스팅을 통해 안드로이드 Jetpack Compose Material3을 사용한 DatePickerDialog를 구현하는데 도움이 됐으면 좋겠습니다.

반응형