foundation.event#
com.himanshoe.kalendar.foundation.event
The event model. KalendarEvent is an interface so you implement it on the type your app already
has; BasicKalendarEvent is there for when you have no such type; byDateExpandingSpans is the
index a grid actually wants.
import com.himanshoe.kalendar.foundation.event.BasicKalendarEventKalendarEvent#
@Stable
public interface KalendarEventAn event tied to a date. Only three members are abstract — date, eventName, and
eventDescription. Everything else defaults to null, so an implementation stays
source-compatible as the model grows.
| Property | Type | Default | Description |
|---|---|---|---|
date | LocalDate | — | The date the event falls on, or the first date for a multi-day event. |
eventName | String | — | Short, human-readable name, e.g. "Team standup". |
eventDescription | String? | — | Optional longer description. Abstract, so it must be implemented — pass null if you have none. |
id | String? | null | A stable identifier, unique across the whole list handed to a calendar. Views that keep per-item state key on it, so supplying it keeps that state attached to the right event across reorders and edits. null makes those views fall back to list position. |
endDate | LocalDate? | null | The last date the event occupies, inclusive, for a multi-day event. null confines it to date. |
startTime | LocalDateTime? | null | When the event begins. Non-null is what makes an event timed: it drives within-day ordering and is what scheduleBlocks lays out. |
endTime | LocalDateTime? | null | When the event ends. A missing end means a one-hour block. |
eventColor | Color? | null | Tints the event's indicator dot and block. null falls back to a default indicator colour. |
calendarId | String? | null | Links the event to a named source ("work", "personal"). Carried as metadata; the built-in views do not read it. |
@Stable is load-bearing: the Compose compiler reads it off the classpath to infer that a
KalendarEvent parameter can be compared rather than assumed changed. That is why the engine depends
on compose.runtime — see What foundation is not.
data class Booking(
val bookingId: Long,
val room: String,
val guest: String,
val from: LocalDateTime,
val to: LocalDateTime,
) : KalendarEvent {
override val id: String get() = bookingId.toString()
override val date: LocalDate get() = from.date
override val eventName: String get() = guest
override val eventDescription: String get() = room
override val startTime: LocalDateTime get() = from
override val endTime: LocalDateTime get() = to
}Note:
startTimeandendTimeareLocalDateTime, notLocalTime— they carry a date as well. Keep their date in sync withdate; the grids place blocks bydate, so a mismatch shows the event on one day with another day's times.
BasicKalendarEvent#
@Immutable
public class BasicKalendarEvent(
override val date: LocalDate,
override val eventName: String,
override val eventDescription: String? = null,
override val startTime: LocalDateTime? = null,
override val endTime: LocalDateTime? = null,
override val eventColor: Color? = null,
override val calendarId: String? = null,
override val endDate: LocalDate? = null,
override val id: String? = null,
) : KalendarEventThe ready-made implementation, for demos, fixtures, and screens that read events from a service and never model them.
| Parameter | Type | Default | Description |
|---|---|---|---|
date | LocalDate | — | The date the event falls on, or the first date of a span. |
eventName | String | — | Short, human-readable name. |
eventDescription | String? | null | Optional longer description. |
startTime | LocalDateTime? | null | Start; non-null makes the event timed. |
endTime | LocalDateTime? | null | End; missing means one hour. |
eventColor | Color? | null | Indicator/block tint. |
calendarId | String? | null | Source-calendar identifier, carried as metadata. |
endDate | LocalDate? | null | Last date occupied, inclusive. |
id | String? | null | Stable list key. |
| Member | Returns | Description |
|---|---|---|
copy(…) | BasicKalendarEvent | A duplicate with only the properties passed here replaced. Takes the same nine parameters as the constructor. |
val day = LocalDate(2026, 8, 12)
val events = listOf(
BasicKalendarEvent(
date = day,
eventName = "Team standup",
eventDescription = "Daily sync",
startTime = LocalDateTime(day, LocalTime(9, 0)),
endTime = LocalDateTime(day, LocalTime(9, 30)),
eventColor = Color(0xFF4CAF50),
),
BasicKalendarEvent(date = day, endDate = day.plus(2, DateTimeUnit.DAY), eventName = "Conference"),
)Warning: this is an
@Immutableclass with a hand-writtencopy, not adata class. It has valueequals,hashCodeandtoString, but nocomponentN— so it cannot be destructured, andcopyis the only generated-looking member it has.KalendarEventitself has nocopyat all, so an event handed back by a drag callback must be smart-cast to a concrete type (or rebuilt from your domain model) before you can change it.
The constructor's positional order is date, eventName, eventDescription, startTime,
endTime, eventColor, calendarId, endDate, id — pass by name and the order stops mattering.
Note: the binary dump lists a
HASH_FACTORfield on this class. It is the31multiplier of the private companion'shashCode, exposed as a JVM static by the Kotlin compiler. It is not part of the Kotlin API and is not visible from Kotlin source.
KalendarEvents#
public typealias KalendarEvents = List<KalendarEvent>A list of events of no particular type — the widest event list there is. It is a plain List, so you
build one with ordinary collection code and any List<YourEventType> already satisfies it.
val events: KalendarEvents = bookings // List<Booking> where Booking : KalendarEventIt is deliberately not parameterised. The views are generic in their event type and spell that
parameter List<E> directly: KalendarEvents<E> would be the same length as List<E>, one more
name to learn, and would force a type argument onto every place that genuinely does not care which
event type it holds. Reach for it when you are storing or passing events without caring what they
are; reach for List<Booking> when the element type is the point — every List<Booking> is already
a KalendarEvents, because List is covariant.
byDateExpandingSpans#
public fun <E : KalendarEvent> List<E>.byDateExpandingSpans(): Map<LocalDate, List<E>>Groups events by every date they occupy: an event with endDate set appears under each date from
date through endDate, inclusive.
The element type is carried through rather than widened. This is the index the calendar views build
before handing a date's events to onDateClick, so widening it here would make every one of those
callbacks report KalendarEvent no matter what the caller supplied:
val byDate: Map<LocalDate, List<Booking>> = bookings.byDateExpandingSpans()This is the index a grid wants. Build it once per event list and each cell becomes a map lookup,
rather than a scan of every event per cell. It is also the difference between a three-day conference
showing on three days and showing only on the first — the bug a hand-written groupBy { it.date }
ships with.
val byDate = remember(events) { events.byDateExpandingSpans() }
monthGridDates(monthStart = monthStart, startDayOfWeek = DayOfWeek.MONDAY).forEach { date ->
DayCell(date = date, events = byDate[date].orEmpty())
}Two edges are handled rather than trusted:
- A span longer than 366 days is truncated, so a malformed
endDatefar in the future cannot turn one event into an unbounded map. - An
endDatebeforedateis treated as a single-day event rather than producing an empty or reversed range.
remember compares its key with equals regardless of Compose stability, so a value-equal list does
not rebuild the map — which is why the built-in views wrap this call exactly as above.