[Dalendar DevLog 4] UI Implementation - Schedule Creation and Editing
Building intuitive UI for schedule management with custom date/time pickers and material design.
![[Dalendar DevLog 4] UI Implementation - Schedule Creation and Editing](/images/blog/dalendar_dev_4_ui.png)
Episode 4: UI Implementation - Schedule Creation and Editing
Introduction
With our solid data layer in place, it’s time to build the user-facing features. This episode covers designing and implementing an intuitive interface for creating, editing, and managing schedules.
1. Material Design Implementation
We follow Material Design 3 principles for a modern, familiar user experience:
1.1. Schedule Creation FAB
FloatingActionButton(
onClick = { showScheduleDialog = true },
containerColor = MaterialTheme.colorScheme.primaryContainer
) {
Icon(Icons.Default.Add, "Add Schedule")
}
1.2. Bottom Sheet Dialog
For schedule creation, we use a modal bottom sheet for easy access without leaving the calendar view:
ModalBottomSheet(
onDismissRequest = { showDialog = false }
) {
ScheduleForm(
onSave = { schedule ->
viewModel.saveSchedule(schedule)
showDialog = false
}
)
}
2. Custom Date Picker
2.1. Rata Die Integration
Our date picker uses Rata Die internally for efficient date selection:
@Composable
fun RataDieDatePicker(
initialRataDie: Int,
onDateSelected: (Int) -> Unit
) {
var selectedRataDie by remember { mutableStateOf(initialRataDie) }
DatePicker(
state = rememberDatePickerState(
initialSelectedDateMillis = rataDieToMillis(selectedRataDie)
)
) { millis ->
selectedRataDie = millisToRataDie(millis)
onDateSelected(selectedRataDie)
}
}
2.2. Performance Optimization
Converting between Rata Die and display formats is optimized using our EAF calculations:
fun rataDieToYMD(rataDie: Int): Triple<Int, Int, Int> {
// Ultra-fast EAF conversion
val n = rataDie - EPOCH_OFFSET
val y = calculateYear(n)
val m = calculateMonth(n, y)
val d = calculateDay(n, y, m)
return Triple(y, m, d)
}
3. Time Picker Implementation
3.1. 12/24 Hour Format Support
@Composable
fun CustomTimePicker(
initialTime: String? = null,
use24HourFormat: Boolean = false,
onTimeSelected: (String) -> Unit
) {
val timeState = rememberTimePickerState(
is24Hour = use24HourFormat
)
TimePicker(state = timeState)
// Format as HH:mm string
val timeString = "${timeState.hour}:${timeState.minute.toString().padStart(2, '0')}"
onTimeSelected(timeString)
}
4. Recurrence UI Builder
4.1. Recurrence Pattern Selector
Users can easily set up recurring events:
@Composable
fun RecurrenceSelector(
onRecurrenceSet: (String?) -> Unit
) {
var frequency by remember { mutableStateOf("NONE") }
var interval by remember { mutableStateOf(1) }
var selectedDays by remember { mutableStateOf(setOf<String>()) }
Column {
// Frequency dropdown
DropdownMenu(
options = listOf("NONE", "DAILY", "WEEKLY", "MONTHLY", "YEARLY")
) { freq -> frequency = freq }
// Weekly day selection
if (frequency == "WEEKLY") {
DayOfWeekSelector(
selectedDays = selectedDays,
onDaysChanged = { selectedDays = it }
)
}
// Generate recurrence rule string
val rule = buildRecurrenceRule(frequency, interval, selectedDays)
onRecurrenceSet(rule)
}
}
4.2. Visual Recurrence Display
fun formatRecurrenceRuleForDisplay(rule: String?): String {
if (rule == null) return "Does not repeat"
val freq = extractFrequency(rule)
val interval = extractInterval(rule)
return when (freq) {
"DAILY" -> if (interval == 1) "Daily" else "Every $interval days"
"WEEKLY" -> {
val days = extractWeekDays(rule)
"Weekly on ${days.joinToString(", ")}"
}
"MONTHLY" -> "Monthly"
"YEARLY" -> "Yearly"
else -> "Custom"
}
}
5. Form Validation
5.1. Input Validation
data class ScheduleFormState(
val title: String = "",
val startRataDie: Int = todayRataDie(),
val endRataDie: Int = todayRataDie(),
val isAllDay: Boolean = true
) {
val isTitleValid: Boolean
get() = title.isNotBlank()
val isDateRangeValid: Boolean
get() = endRataDie >= startRataDie
val isValid: Boolean
get() = isTitleValid && isDateRangeValid
}
5.2. Error Display
OutlinedTextField(
value = formState.title,
onValueChange = { /* update */ },
label = { Text("Title") },
isError = !formState.isTitleValid,
supportingText = {
if (!formState.isTitleValid) {
Text("Title is required")
}
}
)
6. Schedule Display on Calendar
6.1. Day Cell with Schedule Indicators
@Composable
fun DayCell(
rataDie: Int,
schedules: List<Schedule>,
onClick: () -> Unit
) {
Box(
modifier = Modifier
.aspectRatio(1f)
.clickable(onClick = onClick)
) {
// Date number
Text(
text = rataDieToDay(rataDie).toString(),
style = MaterialTheme.typography.bodyMedium
)
// Schedule indicators (dots)
Row(
modifier = Modifier.align(Alignment.BottomCenter)
) {
schedules.take(3).forEach { schedule ->
Box(
modifier = Modifier
.size(4.dp)
.background(
color = Color(schedule.color),
shape = CircleShape
)
)
}
}
}
}
6.2. Day Detail View
When a user clicks a day with schedules:
@Composable
fun DayDetailSheet(
rataDie: Int,
schedules: List<Schedule>,
onDismiss: () -> Unit
) {
ModalBottomSheet(onDismissRequest = onDismiss) {
Column {
Text(
text = formatDate(rataDie),
style = MaterialTheme.typography.headlineSmall
)
LazyColumn {
items(schedules) { schedule ->
ScheduleListItem(
schedule = schedule,
onClick = { /* edit */ }
)
}
}
}
}
}
7. Edit and Delete Operations
7.1. Edit Dialog
fun editSchedule(schedule: Schedule) {
showEditDialog = true
editingSchedule = schedule
}
ScheduleForm(
initialSchedule = editingSchedule,
onSave = { updated ->
viewModel.updateSchedule(updated)
showEditDialog = false
}
)
7.2. Delete Confirmation
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Delete Schedule?") },
text = { Text("This action cannot be undone.") },
confirmButton = {
TextButton(
onClick = {
viewModel.deleteSchedule(scheduleToDelete)
showDeleteDialog = false
}
) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
Text("Cancel")
}
}
)
8. Performance Considerations
8.1. LazyColumn for Schedule Lists
LazyColumn {
items(
items = schedules,
key = { it.id }
) { schedule ->
ScheduleListItem(schedule)
}
}
8.2. Remember and MemoizedState
val schedules by viewModel.schedulesForDay(rataDie)
.collectAsState(initial = emptyList())
val sortedSchedules = remember(schedules) {
schedules.sortedWith(
compareBy({ !it.isAllDay }, { it.startTime })
)
}
9. Accessibility
9.1. Content Descriptions
Icon(
Icons.Default.Add,
contentDescription = "Add new schedule"
)
9.2. Semantic Properties
Box(
modifier = Modifier.semantics {
contentDescription = "Schedule for ${formatDate(rataDie)}"
role = Role.Button
}
)
10. Animation and Transitions
10.1. Dialog Animations
AnimatedVisibility(
visible = showDialog,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
ScheduleDialog()
}
10.2. List Item Animations
AnimatedContent(
targetState = schedule,
transitionSpec = {
slideInHorizontally() with slideOutHorizontally()
}
) { targetSchedule ->
ScheduleListItem(targetSchedule)
}
Conclusion
We’ve built a comprehensive schedule management UI featuring:
✅ Material Design 3 components ✅ Custom date/time pickers with Rata Die integration ✅ Recurrence pattern builder ✅ Form validation and error handling ✅ Schedule indicators on calendar ✅ Edit/delete operations with confirmations ✅ Performance optimizations ✅ Accessibility support ✅ Smooth animations
In the next episode, we’ll implement advanced features like schedule search, filters, and calendar synchronization.
Next Episode Preview:
- Search and filter functionality
- Calendar import/export
- Notification system
- Widget implementation
The app is coming together beautifully!
![[Dalendar DevLog 2] Solid Foundation - Architecture and Data Structure Design](/images/blog/dalendar_dev_2_architecture.png)
![[Dalendar DevLog 3] Database Integration and Schedule Management](/images/blog/dalendar_dev_3_database.png)
![[Dalendar DevLog 7] Advanced Features - Widgets and Notifications](/images/blog/dalendar_dev_7_widgets.png)