
빨간 박스 안의 좌, 우 버튼으로 날짜를 하루씩 옮기고, 날짜를 클릭하면 date picker dialog가 나옵니다!

클릭한 시점의 날짜가 기본적으로 선택된 데이트 피커 등장!
fun ScheduleTabScreen() {
var showDatePicker by remember { mutableStateOf(false) }
var selectedDateTime by remember { mutableStateOf(LocalDateTime.now()) }
KitchingManagerTheme {
/** 생략 */
}
전체 화면이 되어줄 스크린을 만들고 필요한 상태변수들을 만들어줍니다.
/**
* 날짜 선택 컴포넌트
* ex) < 2025-01-26 >
* */
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun DateSelector(
selectedDateTime: LocalDateTime, // 선택된 날짜
onDateChange: (LocalDateTime) -> Unit, // 날짜 변경시 호출
onClickDateBtn: () -> Unit // 아이콘 버튼, 날짜 텍스트 버튼 클릭시 호출
) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Row(
) {
IconButton(onClick = {
onDateChange(selectedDateTime.minusDays(1))
}) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.icon_left_triangle),
contentDescription = "prev date button",
tint = subTextColor
)
}
TextButton(
modifier = Modifier
.padding(13.dp, 0.dp)
.align(Alignment.CenterVertically),
onClick = {
onClickDateBtn()
},
) {
Text(
text = selectedDateTime.toLocalDate().toString(),
color = subTextColor,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
IconButton(onClick = {
onDateChange(selectedDateTime.plusDays(1))
}) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.icon_right_triangle),
contentDescription = "next date button",
tint = subTextColor
)
}
}
}
}
날짜 선택기 컴포넌트는 세개의 인자를 받습니다!

컴포넌트는 요런 모양이 됩니다
왼쪽 세모, 오른쪽 세모를 클릭하면 날짜를 하루씩 조절할 수 있고(onDateChange), 가운데 날짜 텍스트를 누르면 date picker를 띄웁니다(onClickDateBtn)!
다시 스크린으로 돌아와 만들어놓은 날짜 선택기 컴포넌트를 삽입합니다.
DateSelector(
selectedDateTime = selectedDateTime,
onDateChange = { newDate ->
selectedDateTime = newDate
},
onClickDateBtn = {
showDatePicker = true
}
)
selectedDateTime에는 상태변수로 만들어놨던 selectedDateTime을,
onDateChange에는 newDate(LocalDateTime)을 가지고 selectedDateTime을 newDate로 업데이트하는 동작을,
onClickDateBtn에는 showDatePicker를 true로 만들어 다이얼로그를 열어주는 동작을 넣습니다.
전달된 인자들은 날짜 선택기 컴포넌트 내에서 이렇게 쓰입니다.
IconButton(onClick = {
onDateChange(selectedDateTime.minusDays(1))
})
// selectedDateTime에서 하루를 더하거나 빼서
// 다시 selectedDateTime에 세팅합니다.
TextButton(
onClick = {
onClickDateBtn()
}
) {
Text(
text = selectedDateTime.toLocalDate().toString()
)
}
// TextButton에 selectedDateTime의 날짜 부분을 표시하고,
// 클릭 시 showDatePicker를 true로 만듭니다.
fun DatePickerModal(
selectedDateTime: LocalDateTime,
onDismissRequest: () -> Unit,
onClickConfirm: (selectedDateMillis: Long?) -> Unit,
onClickCancel: () -> Unit
) {
val selectedDateInMillis = selectedDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = selectedDateInMillis
)
DatePickerDialog(
onDismissRequest = { onDismissRequest() },
confirmButton = {
TextButton(
onClick = { onClickConfirm(datePickerState.selectedDateMillis) }
) {
Text(
text = "확인",
color = mainColor,
fontWeight = FontWeight.Bold,
fontFamily = pretendard
)
}
},
dismissButton = {
TextButton(
onClick = { onClickCancel() }
) {
Text(
text = "취소",
color = mainColor,
fontWeight = FontWeight.Bold,
fontFamily = pretendard
)
}
},
colors = DatePickerDefaults.colors(
containerColor = Color.White,
)
) {
DatePicker(
state = datePickerState,
}

(색은 원래 이렇지는 않고요... Material Theme을 따라갑니다. 저는 디자이너님께 어느부분 색을 바꿀 수 있는지 전달해드리기 위해 알록달록하게 만들어놨습니다...)
datePickerModal이라는 컴포넌트를 만들었습니다.
인자로 세개를 받는데요,
컴포넌트 안에 두개의 변수가 또 따로 있는데요,
xml에서는 분명 year, month, date를 따로 숫자로 다뤘는데, 왜 갑자기 날짜를 다 합쳐 밀리초로 다루게 되었는지는 모르겠습니다...
val selectedDateInMillis = selectedDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
LocalDateTime.atZone(-ZoneId-)은 이 LocalDateTime의 타임존을 -여기-로 설정한 ZonedDateTime으로 바꿔줍니다.
그 ZonedDateTime을 toEpochMilli()를 통해 밀리초로 바꾸어 사용합니다.
그렇게 바꾼 밀리초를 rememberDatePickerState에 사용해 초기값을 selectedDateTime(을 밀리초로 바꾼 값)으로 적용해줍니다.
뭐... 안되는건 아닙니다.
다만 LocalDate에는 Time에 관한 정보가 없어서 Time 정보가 00:00:00으로 들어가는데요,
밀리초로 변환하는 과정에서, UTC 기준으로 변환을 거치게 되는데 UTC는 우리나라보다 9시간이 빠집니다.
한국의 n일 00시는 UTC 기준으로 바꾸면 n-1일 오후 3시가 되니까, 날짜가 하루 적게 변환되는 일이 생깁니다.
LocalDate에서 이런 현상을 방지하려면... 사실 간단하게는 타임존을 설정할때 atStartOfDay(ZoneId.of("UTC"))로 타임존을 UTC로 설정하면 되는데요.
한국에서 사용할 앱을 개발하는데 타임존을 UTC로 설정하는게 저는 도저히 납득이 안돼서(...) LocalDateTime을 사용했습니다.
근데 뭐... 사용자 입장에선 딱히 상관 없겠죠? 융통성 있게 개발하는것도 중요한 것 같기도 하고요. 하지만 저는 안되겠습니다...
우리가 구현한 컴포넌트인 DatePickerModal 안에는 DatePickerDialog가 쓰입니다.
이 DatePickerDialog는 content로 컴포저블을 받습니다.

content에 DatePicker 컴포저블을 넣어주면 됩니다.
DatePickerDialog 안에 DatePicker를 넣어준다고 생각하시면 됩니다.

DatePickerDialog는 DatePicker를 다이얼로그로 띄워주기 위한 컴포저블입니다.
사진에서 빨간 네모로 표시한 부분이 다이얼로그 컨테이너에 해당하는 부분입니다.
특이하게도 인자로 컴포저블 함수 dismissButton과 confirmButton을 받는데요, 컴포저블로 취소버튼, 확인버튼을 커스텀 할 수 있습니다. 물론 각 버튼에 해당하는 onClick도 따로 구현해줘야 합니다.
confirmButton = {
TextButton(
onClick = {
onClickConfirm(datePickerState.selectedDateMillis) }
) {
Text(
text = "확인"
)
}
},
dismissButton = {
TextButton(
onClick = { onClickCancel() }
) {
Text(
text = "취소"
)
}
},
부모 컴포넌트에 있는 공통 상태변수를 관리해줘야 해서 인자로 받아온 onClickConfirm, onClickCancel을 이용해 onClick을 구현했습니다.
onDismissRequest도 필수로 넣어줘야하는 인자값입니다.

사진에서 파란 네모로 표시한 부분이 DatePicker 부분입니다.
필수로 넣어줘야하는 인자는 DatePickerState 타입의 state입니다.
아까 만들어뒀던 rememberDatePickerState를 전달하면 알아서 state를 이용해 선택된 날짜를 관리합니다. 저는 DatePickerDialog의 confirmButton의 onClick에 선택된 날짜를 이용해 selectedDateTime을 업데이트하는 동작을 넣어주었습니다.
// DatePickerState
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = selectedDateInMillis
)
// confirmButton에 전달한 onClick 이벤트
// 전달받은 selectedDateMillis를 LocalDateTime으로 바꾸어 selectedDateTime을 업데이트 시킴
// selectedDateMillis은 nullable(사용자가 데이트 피커에서 날짜를 선택하지 않고 데이트 피커를 닫은 경우엔 null)이기 때문에
// 필요한 경우 null처리를 해주어야 해요
// 이 코드의 경우 selectedDateTime이 null을 허용하지 않기 때문에
// null 처리가 필수였습니다!
onClickConfirm = { selectedDateMillis ->
if(selectedDateMillis !== null) {
selectedDateTime =
LocalDateTime.ofInstant(
java.time.Instant.ofEpochMilli(selectedDateMillis),
java.time.ZoneId.systemDefault()
)
}
showDatePicker = false
}
// onClick 이벤트의 인자로 datePickerState의 selectedDateMillis를 전달
onClick = { onClickConfirm(datePickerState.selectedDateMillis) }
다시 스크린으로 돌아와 DatePickerModal 컴포넌트를 삽입합니다.
if (showDatePicker) {
DatePickerModal(
selectedDateTime = selectedDateTime,
onDismissRequest = { showDatePicker = false },
onClickConfirm = { selectedDateMillis ->
if(selectedDateMillis !== null) {
selectedDateTime =
LocalDateTime.ofInstant(
java.time.Instant.ofEpochMilli(selectedDateMillis),
java.time.ZoneId.systemDefault()
)
}
showDatePicker = false
},
onClickCancel = { showDatePicker = false }
)
}
DatePicker 만들기 끝!