본인이 맡은 기능과 뷰에 대한 설명
‘회원가입’ 뷰와 ‘수현이 찾기’ 뷰에서, Bottom Sheet에서 Picker 형태로 날짜 및 시간을 입력해야 하는 컴포넌트가 있었습니다.
‘회원가입’ 뷰에서의 YearPicker는 연도를 나타내는 1개의 Picker로 구성되며, ‘수현이 찾기’ 뷰에서의 DateTimePicker는 총 4개의 Picker로 구성되며, 각각 날짜, 오전 오후, 시, 분을 나타내야 했고, 각 Picker의 중앙 item에 box 형태로 배경 처리가 되어야 했습니다.
처음에는 Material3의 일반적인 Picker를 활용할까 하다가, 컴포넌트 커스텀을 경험해보고 싶은 생각에 도전하게 되었습니다.
이에 따라 위 형태의 Picker를 제공하는
GitHub - kez-lab/Compose-DateTimePicker: Compose-DateTimePicker
위 라이브러리를 활용하게 되었습니다!
라이브러리를 활용하려면,
// Date Time Picker
implementation(libs.compose.date.time.picker)
implementation(libs.kotlinx.datetime)
위 두 가지 dependency를 추가하고,
import com.kez.picker.Picker
import com.kez.picker.PickerState
import com.kez.picker.rememberPickerState
위 라이브러리를 활용하기 위해 import문들을 추가하였습니다.
위 컴포넌트를 활용하면, 태어난 연도 Picker는 ‘2005’와 같은 Int 형태로, 날짜 및 시간 Picker는
2025-01-15T14:30와 같은 LocalDateTime 형태로 데이터를 넘겨야 하였습니다.
해당 기능 및 뷰를 개발하며 만난 문제 상황
라이브러리의 Picker는 다음과 같이 생겼습니다.
visibleItemsCount, isInfinity, textStyle, selectedTextStyle 등을 활용하여 이를 커스텀하려 하였고, 생각보다 Picker의 형태가 다양해서 이 과정에서 어려움을 겪었던 것 같습니다.
이를 위해 각각의 PickerState를 추가하였습니다.
datePickerState: PickerState<String> = remember {
PickerState(
"${startDateTime.monthNumber}월 ${startDateTime.dayOfMonth}일 ${startDateTime.dayOfWeek.name.toKoreanDay()}"
)
},
hourPickerState: PickerState<Int> = remember {
val roundedUpHour = if ((startDateTime.minute + 4) / 5 * 5 >= 60) {
(if (startDateTime.hour % 12 == 11) 12 else (startDateTime.hour + 1) % 12)
} else {
if (startDateTime.hour == 0) 12 else startDateTime.hour % 12
}
PickerState(roundedUpHour)
},
minutePickerState: PickerState<String> = remember {
val minute = ((startDateTime.minute + 4) / 5) * 5
PickerState(if (minute == 60) "00" else minute.toString().padStart(2, '0'))
},
amPmPickerState: PickerState<String> = remember {
PickerState(if (startDateTime.hour in 0..11) "오전" else "오후")
},
→ 위와 같은 형태로 각각의 초기값을 넣어주고, minutePickerState 같은 경우에는 5 단위로 가야 했기 때문에, ((startDateTime.minute + 4) / 5) * 5
를 통해 이를 반영하였습니다.
그리고 이에 따라 각 item들의 형식을,
dateItems: List<String> = remember {
val endDate = LocalDate(2025, 12, 31)
if (currentDate > endDate) emptyList()
else {
val totalDays = endDate.toEpochDays() - currentDate.toEpochDays() + 1
(0 until totalDays).map { offset ->
val date = currentDate.plus(DatePeriod(days = offset))
"${date.monthNumber}월 ${date.dayOfMonth}일 ${date.dayOfWeek.name.toKoreanDay()}"
}
}
},
amPmItems: List<String> = listOf(stringResource(R.string.am), stringResource(R.string.pm)),
hourItems: List<Int> = HOUR12_RANGE,
minuteItems: List<String> = (0..55 step 5).map { it.toString().padStart(2, '0') },
→ 초기 형식과 동일하게끔 설정하였습니다.
발생한 문제 :
datePickerState.selectedItem, amPmPickerState.selectedItem, hourPickerState.selectedItem, minutePickerState.selectedItem
위 코드와 같은 형태로 호출하면, 각각의 selectedItem의 값을 알 수 있는데, 문제는 index가 하나씩 차이 나는 형태로 인식되는 것이 문제였습니다.
예상했던 이유 1 : 상태 관리 문제
예상했던 이유 2 : selectedItem을 둘러싸는 Box 때문에 레이아웃에 변화가 생겨서 값이 제대로 인식이 되지 않는 것인지
예상했던 이유 3 : isInfinity라는 속성이, 각 Picker를 scroll 했을 때 다시 처음 값으로 돌아오게끔 무한으로 돌아가는지, 아니면 처음과 끝이 있는 형태인지를 나타내는데, ‘오전, 오후’ Picker를 제외한 모든 Picker는 isInfinity가 false인데, 나머지 Picker들은 isInfinity가 true이기 때문에, 이 과정에서 생기는 간극 때문에 상태 관리에 문제가 발생하는 것인지
등, 다양한 이유를 고려해보았습니다.
이에 따라, 하나씩 로그를 찍어가며 값을 확인해보았습니다.
본인만의 해결 방식 + 문제를 해결해 나가면서 배운 점
결론적으로, 이 라이브러리의 Picker의 기본 커스텀 형식 때문에, Picker의 레이아웃에 문제가 생겨, 값을 제대로 인식하지 못하는 것임을 알게 되었습니다.
더 나아가, 다른 화면에서는 잘리지 않는 text가, 아무리 영역이 충분해도 약간씩 잘리는 문제가 발생하였습니다.
그래서, 아예 라이브러리를 하나씩 톺아보기로 하였고, 이 과정에서 문제점을 발견할 수 있었습니다.
Picker의 기본 형태
@Composable
fun <T> Picker(
items: List<T>,
modifier: Modifier = Modifier,
state: PickerState<T>,
startIndex: Int = 0,
visibleItemsCount: Int = 3,
textModifier: Modifier = Modifier,
textStyle: TextStyle = LocalTextStyle.current,
selectedTextStyle: TextStyle = LocalTextStyle.current,
dividerColor: Color = LocalContentColor.current,
itemPadding: PaddingValues = PaddingValues(8.dp),
fadingEdgeGradient: Brush = Brush.verticalGradient(
0f to Color.Transparent,
0.5f to Color.Black,
1f to Color.Transparent
),
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
itemTextAlignment: Alignment.Vertical = Alignment.CenterVertically,
dividerThickness: Dp = 1.dp,
dividerShape: Shape = RoundedCornerShape(10.dp),
isInfinity: Boolean = true
)
→ 위 형태를 보면, dividerThickness가 기본으로 1.dp가 들어가 있는데, 이를 뷰에서
dividerColor = Transparent
로만 적용하여 관리해주고 있었고, 결론적으로 Transparent이지만 실제 그 자리에 divider가 있음으로써 레이아웃에 문제를 일으켜서 값이 하나씩 index가 차이나게끔 인식되는 것이었습니다.
결론적으로
itemPadding: PaddingValues = PaddingValues(top = 11.dp, bottom = 15.dp, start = 5.dp, end = 5.dp),
selectedItem의 padding 값을, 원래 피그마 상의 값보다 1씩 줄여서 반영하니까, 글자 잘림 현상도 해결되었으며, 인식 값도 문제 없이 인식이 되게끔 해결되었습니다 !
이번 Picker 커스텀을 통해, 라이브러리를 활용해보는 것도 매우 좋은 경험이고 재미있는 경험이었으며, 라이브러리 상에서의 문제점 및 수정되면 좋을 만한 부분을, 라이브러리를 직접 톺아보면서 공부해보는 좋은 경험이었다고 생각합니다.