Kalendar

kalendar.state#

com.himanshoe.kalendar.state

Navigation state, selection state, and the clock. Each state class wraps a Compose scroll state and exposes it in date terms; each is created by a remember* factory rather than a constructor, so it survives configuration changes and is keyed on the things that should rebuild it.

kotlin
import com.himanshoe.kalendar.state.rememberKalendarMonthState

KalendarViewState#

kotlin
@Stable
public class KalendarViewState

The paged views' state — a PagerState and a KalendarPager, presented as dates. Returned by rememberKalendarWeekState, rememberKalendarMonthState, rememberKalendarYearState and rememberKalendarScheduleState.

MemberTypeDescription
visibleDateLocalDateStart date of the page currently on screen. Backed by derivedStateOf, so reading it only invalidates on a page change.
startDayOfWeekDayOfWeekThe week's first column, as the state was built with.
minDateLocalDate?The lower bound this state was created with.
maxDateLocalDate?The upper bound.
canScrollBackwardBooleanWhether stepping one page back stays within minDate.
canScrollForwardBooleanWhether stepping one page forward stays within maxDate.
timeSourceKalendarTimeSourceWhere this state reads "today" from. Read-only, but snapshot-backed: the remember*State factory keeps it in step with the ambient source without recreating the state and losing the scroll position. Choose the source by passing it to the factory.
suspend animateScrollTo(date)UnitAnimates to the page containing date, clamped to the bounds.
suspend animateScrollToToday()UnitAnimates to the page containing today, clamped.
suspend animateToPreviousPage()UnitOne page back. A no-op past minDate.
suspend animateToNextPage()UnitOne page forward. A no-op past maxDate.
kotlin
val state = rememberKalendarMonthState(initialDate = today)
val scope = rememberCoroutineScope()

Text("${state.visibleDate.month} ${state.visibleDate.year}")
KalendarMonth(selectedDate = today, state = state)
Button(onClick = { scope.launch { state.animateScrollTo(LocalDate(2026, 12, 25)) } }) {
    Text("Jump to December")
}

Note: there is deliberately no today property here. A clock-derived value stored on a @Stable object notifies nobody when the clock moves on, so it would go stale at midnight and no recomposition would ever be scheduled to fix it. Read it in composition instead: rememberKalendarToday(state.timeSource).

KalendarTimelineState#

kotlin
@Stable
public class KalendarTimelineState

KalendarTimeline's state — a LazyListState and a KalendarPager. A timeline scrolls continuously, so it has no pages and therefore no canScrollBackward/Forward and no per-page animation.

MemberTypeDescription
visibleDateLocalDateStart of the month currently topmost in the list.
startDayOfWeekDayOfWeekThe week's first column.
minDateLocalDate?The lower bound.
maxDateLocalDate?The upper bound.
timeSourceKalendarTimeSourceWhere this state reads "today" from. Read-only, for the same reason as KalendarViewState.timeSource.
suspend animateScrollTo(date)UnitAnimates so the month containing date reaches the top.
suspend animateScrollToToday()UnitAnimates back to today's month.
kotlin
val state = rememberKalendarTimelineState(initialDate = today)
KalendarTimeline(selectedDate = today, state = state)

Warning: minDate/maxDate bound the buttons, not the swipe. On the paged views they disable the header's arrows and clamp animateScrollTo, but a swipe can carry you past them. On KalendarTimeline the bounds do clamp the scroll itself, because the list is built with a finite item count.

The factories#

All five take the same parameters, except rememberKalendarScheduleState, which has no startDayOfWeek — a single day has no week columns to align.

ParameterTypeDefaultDescription
initialDateLocalDatetoday, via timeSourceThe week / month / year / day initially visible.
startDayOfWeekDayOfWeekDayOfWeek.MONDAYThe first week column. Not on rememberKalendarScheduleState.
minDateLocalDate?nullLower bound. Unbounded when null.
maxDateLocalDate?nullUpper bound. Unbounded when null.
timeSourceKalendarTimeSourcerememberKalendarTimeSource()The seam between the calendar and the wall clock.
FactoryReturnsPages by
rememberKalendarWeekState(…)KalendarViewStateWeek. Shared by KalendarWeek and KalendarScheduleWeek.
rememberKalendarMonthState(…)KalendarViewStateMonth. Also used by KalendarDatePicker.
rememberKalendarYearState(…)KalendarViewStateYear.
rememberKalendarScheduleState(…)KalendarViewStateDay. Used by KalendarSchedule and KalendarResourceView.
rememberKalendarTimelineState(…)KalendarTimelineStateNothing — free scroll.
kotlin
val state = rememberKalendarMonthState(
    initialDate = today,
    startDayOfWeek = DayOfWeek.SUNDAY,
    minDate = LocalDate(2020, 1, 1),
    maxDate = LocalDate(2030, 12, 31),
)

initialDate defaults to today as read through timeSource, so wrapping a screen in ProvideKalendarTimeSource(fixed) { … } pins every view's idea of the current date at once.

Selection state#

KalendarSelectionState#

kotlin
@Stable
public class KalendarSelectionState

A rememberSaveable-backed holder over KalendarSelection. It implements the tap behaviour of one KalendarSelectionMode and survives configuration changes and process death — including a half-finished range.

MemberTypeDescription
selectedDatesSet<LocalDate>The current selection. Pass it to a view's selectedDates.
onDateClick(date)UnitReduces a tap under the mode this state was created with.
onRangeDrag(start, current)UnitUpdates a live press-and-drag range. Call on every pointer move.
onRangeDragEnd()UnitEnds such a drag, so the next tap starts a fresh range.
clear()UnitEmpties the selection, including any half-finished range.

rememberKalendarSelectionState#

kotlin
@Composable
public fun rememberKalendarSelectionState(
    mode: KalendarSelectionMode = KalendarSelectionMode.Single,
    initialSelection: Set<LocalDate> = emptySet(),
): KalendarSelectionState
ParameterTypeDefaultDescription
modeKalendarSelectionModeSingleWhat a tap means. Changing it resets the state, since it is the rememberSaveable key.
initialSelectionSet<LocalDate>emptySet()What is selected before the first tap.
kotlin
val selection = rememberKalendarSelectionState(mode = KalendarSelectionMode.Range)

KalendarMonth(
    selectedDate = today,
    selectedDates = selection.selectedDates,
    onDateClick = { date, _ -> selection.onDateClick(date) },
    onDateRangeSelect = selection::onRangeDrag,
    onDateRangeSelectEnd = selection::onRangeDragEnd,
)

For anything the three modes do not cover — a minimum range length, disallowed end dates — hold the Set<LocalDate> yourself, or reduce a KalendarSelection directly.

The clock#

Every view, state factory and now-indicator reads "today" through a KalendarTimeSource rather than calling Clock.System.

ProvideKalendarTimeSource#

kotlin
@Composable
public fun ProvideKalendarTimeSource(
    timeSource: KalendarTimeSource,
    content: @Composable () -> Unit,
)

Provides a time source to every view inside. The single injection point — a view's state, its initialDate default, and its now-indicator all end up reading the same clock.

ParameterTypeDefaultDescription
timeSourceKalendarTimeSourceThe source to provide. remember it.
content@Composable () -> UnitThe subtree that reads it.
kotlin
val fixed = remember {
    KalendarTimeSource(
        timeZone = TimeZone.UTC,
        clock = object : Clock {
            override fun now(): Instant = Instant.parse("2026-03-10T10:20:00Z")
        },
    )
}

ProvideKalendarTimeSource(fixed) {
    KalendarMonth(selectedDate = LocalDate(2026, 3, 12))
}

Every picture on this documentation site is generated this way.

rememberKalendarTimeSource#

kotlin
@Composable
public fun rememberKalendarTimeSource(): KalendarTimeSource

The ambient time source, or a composition-scoped default when nothing provided one.

kotlin
val timeSource = rememberKalendarTimeSource()

rememberKalendarToday#

kotlin
@Composable
public fun rememberKalendarToday(
    timeSource: KalendarTimeSource = rememberKalendarTimeSource(),
): LocalDate

Today's date, re-read when the day changes — so a calendar left open overnight highlights the new day instead of yesterday's.

ParameterTypeDefaultDescription
timeSourceKalendarTimeSourcethe ambient oneWhere to read the date from.
kotlin
val today = rememberKalendarToday(state.timeSource)

rememberKalendarNow#

kotlin
@Composable
public fun rememberKalendarNow(
    timeSource: KalendarTimeSource = rememberKalendarTimeSource(),
    tick: Duration = 1.minutes,
): LocalDateTime

The current date and time, re-read every tick. This is what a now-indicator is positioned from.

ParameterTypeDefaultDescription
timeSourceKalendarTimeSourcethe ambient oneWhere to read from.
tickDuration1.minutesHow often to re-read.
kotlin
val now = rememberKalendarNow(tick = 30.seconds)

Warning: this recomposes its caller every tick. The built-in grids instead hold the reading as a State and read it inside Modifier.offset { }, so a minute passing re-places one line rather than recomposing the indicator, its slot content, and everything above the read. Prefer that shape if you are building your own hour grid.