Daily Agenda View is a clone of the calendar component seeing in the Microsoft Outlook App or Microsoft Teams App. The component layouts daily events across an hourly based timeline. It is great for task planner apps, appointment apps, event schedule apps, calendar apps.
Live demo using kotlin-wasm: Daily Agenda View
| Platform Support | Kotlin Compatibility | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
Add the gradle coordinates:
sourceSets {
commonMain.dependencies {
implementation("io.github.pablichjenkov:daily-agenda-view:<latest-version>")
}
}In Android only projects which already use java-time or joda-time, it is necessary to include the kotlinx-datetime dependecy too. So gradle will look like bellow.
dependencies {
implementation("io.github.pablichjenkov:daily-agenda-view:<latest-version>")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
}The library API is based on the compose StateController pattern. A StateController is basically a mini MVI store just for a specific UI component. Instead of being coupled to a full screen single gigantic state. The state controller is only bound to a specific composable ui section of the screen. These are the options from the library:
The TimeSlotsStateController displays events in a hour and minute based timeline.
val timeSlotsStateController = remember {
TimeSlotsStateController(
timeSlotConfig = TimeSlotConfig(slotScale = 2, slotHeight = 48),
eventsArrangement =
EventsArrangement.MixedDirections(EventWidthType.FixedSizeFillLastEvent)
).apply {
timeSlotsDataUpdater.postUpdate {
addEvent(
uuid = Uuid.random(),
startTime = LocalTime(hour = 8, minute = 0),
endTime = LocalTime(hour = 8, minute = 30),
title = "Event 0",
description = "Description 0"
)
addEventList( // When adding a list all events must belong to the same slot.
startTime = LocalTime(hour = 8, minute = 0),
events =
listOf(
LocalTimeEvent(
uuid = Uuid.random(),
startTime = LocalTime(hour = 8, minute = 0),
endTime = LocalTime(hour = 8, minute = 45),
title = "Event 1",
description = "Description 1"
),
LocalTimeEvent(
uuid = Uuid.random(),
startTime = LocalTime(hour = 8, minute = 0),
endTime = LocalTime(hour = 9, minute = 0),
title = "Event 2",
description = "Description 2"
)
)
)
}
}
}
Now that you create a TimeSlotsStateController and added some events to it. Then add a TimeSlotsView in your Composable screen.
@Composable
fun MyDayScheduleView(modifier = Modifier.fillMaxSize()) {
val timeSlotsStateController = remember { ... }
TimeSlotsView(timeSlotsStateController = timeSlotsStateController) { localTimeEvent ->
Box(modifier = Modifier.fillMaxSize().padding(all = 2.dp).background(color = Color.Gray)) {
Text(
text =
"${localTimeEvent.title}: ${localTimeEvent.startTime}-${localTimeEvent.endTime}",
fontSize = 12.sp
)
}
}
}
1. In this mode the agenda view will try to maximize the events witdh. It achieves that by mixing the rows layout direction. Even rows are rendered from left to right while odd rows are rendered from right to left. Since the events are order by duration, this mode leverage the maximum space available by laying out in the opposite direction from the previous road. It should be very effective in most data use cases.
eventsArrangement = EventsArrangement.MixedDirections(eventWidthType = EventWidthType.VariableSize)
2. Similar to above, this mode also mixes the direction of the layout, even rows do LTR and odd rows fo RTL. But in this mode all the events have the same with. This is for the case where maximum space wants to be coverred but at the same time esthetic is needed.
eventsArrangement = EventsArrangement.MixedDirections(eventWidthType = EventWidthType.FixedSize)
3. This mode is just like number 2 but expand the single slot events to occupy the full row available width. This is the default configuration if you don't specify any.
eventsArrangement = EventsArrangement.MixedDirections(eventWidthType = EventWidthType.FixedSizeFillLastEvent)| Events align with slot start/end time | Events do not align with slot start/end time |
|
|
4. Instead of maximizing space consumption, an App might want consistency laying out the daily calendar events. Bellow mode renders from left to right always and also expand the single slot events to occupy the full row available width.
eventsArrangement = EventsArrangement.LeftToRight(lastEventFillRow = true)
5. Similar to number 4 but in this case we want all the events to have the same width.
eventsArrangement = EventsArrangement.LeftToRight(lastEventFillRow = false)
6. The same as number 4 but from Right to left. Could be useful in countries where languages are written/read from right to left.
eventsArrangement = EventsArrangement.RightToLeft(lastEventFillRow = true)
7. The same as number 5 but from Right to left.
eventsArrangement = EventsArrangement.RightToLeft(lastEventFillRow = false)
The DecimalSlotsStateController displays events in a vertical decimal axis. Although use cases for this type of data presentation are rare, the library includes it anyway in case someone needs it. This is actually the base StateController in which the TimeSlotsStateController builds upon.
DecimalSlotsStateController(
decimalSlotConfig =
DecimalSlotConfig(
initialSlotValue = 7.0F,
lastSlotValue = 19.0F,
slotScale = 2,
slotHeight = 48
),
eventsArrangement = EventsArrangement.MixedDirections(EventWidthType.FixedSizeFillLastEvent)
)
.apply {
decimalSlotsDataUpdater.postUpdate {
addDecimalEvent(
DecimalEvent(
uuid = Uuid.random(),
title = "Ev0",
description = Constants.EmptyDescription,
startValue = 8.5F,
endValue = 10.0F
)
)
addDecimalEventList(
startValue = 8.0F,
segments = listOf(
DecimalEvent(
uuid = Uuid.random(),
title = "Ev1",
description = Constants.EmptyDescription,
startValue = 8.0F,
endValue = 10.0F
),
DecimalEvent(
uuid = Uuid.random(),
title = "Ev2",
description = Constants.EmptyDescription,
startValue = 8.0F,
endValue = 9.5F
)
)
)
}
}
Then use the DecimalSlotsView Composable like bellow:
// Assuming the StateController is hosted in a ViewModel
val decimalSlotsStateController = viewModel.decimalSlotsStateController
DecimalSlotsView(decimalSlotsStateController = decimalSlotsStateController) { decimalEvent ->
Text(text = "${decimalEvent.title}: ${decimalEvent.startValue}-${decimalEvent.endValue}", fontSize = 12.sp)
}Above code should produce something like the image bellow. Notice the axis values are just decimal numbers, also this component doesn't have a current time line indicator.
The library also includes an EpgSlotsStateController, EPG(Electronic Guide Program) is a very popular component in TV apps. Although other uses cases can leverage this type of component too.
val epgSlotsStateController = remember {
EpgSlotsStateController(
EpgChannelSlotConfig(
timeSlotConfig =
TimeSlotConfig(startSlotTime = LocalTime(6, 0), endSlotTime = LocalTime(23, 59))
)
)
.apply {
epgSlotsDataUpdater.postUpdate {
addChannel(
EpgChannel(
name = "Ch1",
events =
listOf(
LocalTimeEvent(
uuid = Uuid.random(),
title = "Ev1",
description = Constants.EmptyDescription,
startTime = LocalTime(9, 0),
endTime = LocalTime(10, 0)
),
LocalTimeEvent(
uuid = Uuid.random(),
title = "Ev2",
description = Constants.EmptyDescription,
startTime = LocalTime(10, 0),
endTime = LocalTime(11, 30)
)
)
)
)
addChannel(
EpgChannel(
name = "Ch2",
events =
listOf(
LocalTimeEvent(
uuid = Uuid.random(),
title = "Ev3",
description = Constants.EmptyDescription,
startTime = LocalTime(9, 30),
endTime = LocalTime(10, 15)
),
LocalTimeEvent(
uuid = Uuid.random(),
title = "Ev4",
description = Constants.EmptyDescription,
startTime = LocalTime(10, 30),
endTime = LocalTime(11, 0)
)
)
)
)
}
}
}
Then use the EpgSlotsView to render the EpgSlotsStateController instance.
@Composable
fun MyTvScheduleView(modifier = Modifier.fillMaxSize()) {
val epgSlotsStateController = remember { ... }
EpgSlotsView(epgSlotsStateController = epgSlotsStateController) { localTimeEvent ->
Text(
text = "${localTimeEvent.title}: ${localTimeEvent.startTime}-${localTimeEvent.endTime}",
fontSize = 12.sp
)
}
}
We welcome contributions from the community! If you have ideas for new features, bug fixes, or improvements, please open an issue or submit a pull request.

