This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Always respond in Korean (한국어) when interacting with this codebase. All explanations, summaries, documentation, and guidance should be written in Korean. Code identifiers, file/folder names, and technical terms should remain in English, but conceptual explanations and context should be in Korean.
Compose-DateTimePicker is a Kotlin Multiplatform library providing date and time picker components for Compose. It targets Android, iOS, Desktop (JVM), and Web with a shared codebase, published to Maven Central as io.github.kez-lab:compose-date-time-picker.
Repository VERSION_NAME: 0.6.0 License: Apache 2.0
The library follows a composition-based architecture with a single generic Picker<T> component as the foundation:
Picker<T> (generic scrollable picker)
↓ composed into
TimePicker (hour + minute + optional AM/PM)
YearMonthPicker (year + month)
DatePicker (year + month + day)
Key pattern: Higher-level components (TimePicker, YearMonthPicker) are compositions of multiple controlled Picker instances, not subclasses. Generic Picker<T> receives selectedItem and onSelectedItemChange from the caller; higher-level state classes own only logical date/time values.
Picker.kt (~440 lines)
- Generic scrollable picker using
LazyColumnwith snap behavior - Supports infinite (
isInfinity=true) and bounded scrolling - Uses a bounded cyclic buffer (
items.size * 1000) for infinite mode - Fading edge effect via
graphicsLayer+BlendMode.DstIn - State updates via
snapshotFlow { firstVisibleItemIndex }inLaunchedEffect
State files
time/TimePickerState.ktcontainsTimePickerStateandrememberTimePickerStatedate/DatePickerState.ktcontainsDatePickerStateandrememberDatePickerStatedate/YearMonthPickerState.ktcontainsYearMonthPickerStateandrememberYearMonthPickerStateTimePickerStateexposesselectedTime: LocalTimeandselectedHourOfDayYearMonthPickerStateexposesselectedYearMonth: YearMonth;selectedMonthDate: LocalDateis for interoperability with date APIs- Specialized picker states use saveable state; generic
Picker<T>is controlled by caller-owned state because arbitraryTis not guaranteed saveable
DatePicker.kt / DatePickerState.kt
- Compose year, month, and day pickers
- Clamp selected day when the selected year/month has fewer days
DatePickerStateexposesselectedDate: LocalDate
TimePicker.kt / YearMonthPicker.kt
- Compose multiple
Pickerinstances in aRow - Handle 12/24-hour format conversion (TimePicker only)
- Use
kotlinx-datetimefor date/time utilities
User scrolls LazyColumn
→ LazyListState.firstVisibleItemIndex changes
→ snapshotFlow emits new index
→ Picker calls onSelectedItemChange(item)
→ caller-owned selectedItem updates
→ Recomposition shows updated UI
datetimepicker/src/
├── commonMain/kotlin/ # Shared UI and logic
├── androidMain/ # Android-specific (UI tooling preview)
├── iosMain/ # iOS-specific (currently minimal)
├── desktopMain/ # Desktop-specific (currently minimal)
├── jsMain/ # Web-specific source set directory, currently minimal
└── commonTest/ # Shared unit tests
Most logic lives in commonMain. Platform-specific code is minimal.
- Use
feature/*branch names for new implementation work in this repository. - Treat
mainas the integration branch. There is currently nodevelopbranch on the remote. - After a substantial implementation step, run a six-agent feedback loop when the maintainer asks for autonomous improvement work: collect feedback, fix actionable issues, verify again, then open or update the PR.
- Merge PRs only after relevant local verification and GitHub Actions checks pass.
- Keep improving toward Android developer ergonomics first: state APIs, sample usability, documentation clarity, accessibility, and predictable behavior in real app lifecycles.
- Before adding custom performance machinery, verify that the standard Compose/runtime primitives are insufficient. Prefer
remember,derivedStateOf,snapshotFlow, stable object ownership, and domain helpers before introducing bespoke caches or dirty-check classes. If the reason is performance, record the measured or directly inspected evidence in the PR. - Treat bespoke caching, manual invalidation, synchronization, and equality-key logic as high-risk code. If such code remains, add focused regression coverage for stale data, source identity changes, non-default column orders, and state changes that should invalidate derived values. Private code still needs tests when it replaces framework guarantees.
- When the same invariant appears in multiple components, extract the common rule or deliberately audit every counterpart. Keep
contains,coerce*, validation, rendered item filtering, and selection repair behavior aligned across TimePicker, DatePicker, YearMonthPicker, and DateRangePicker. - Before handing off a PR for review, red-team the implementation against the assumptions used to build it: look for unmeasured optimization, duplicated local patterns, missing fast paths, missing tests around non-standard logic, and inconsistent failure contracts.
- During 0.x API stabilization, prefer the best long-term API shape over source compatibility when the maintainer explicitly authorizes breaking changes. Document every breaking change in README/CHANGELOG and update ABI dumps.
- Keep public state APIs colocated with their component package unless there is a strong API-design reason not to. For example,
TimePickerStateandrememberTimePickerStatelive incom.kez.picker.time; date and year-month state APIs live incom.kez.picker.date. - Prefer controlled picker APIs with a single source of truth. Avoid APIs where both a state object and positional parameter can initialize or mutate the same selection.
- For composite pickers, expose a state object for saveable logical selection plus an optional
onSelected*Changecallback for user-driven changes. Document that programmaticstate.select*calls require the caller to update app-owned state in the same event handler. - For year/month-only values, prefer
date.YearMonthover encoding the selection as the first day of aLocalDate; keepselectedMonthDateonly as an interop convenience. - Keep repeated visual/layout picker configuration grouped in
PickerStyleinstead of expanding the public composable parameter list. New visual options should usually be added toPickerDefaults.style(...)and documented as a breaking API change while the library is still 0.x. - Keep interaction availability as a top-level
enabledparameter on generic and composite pickers. Disabled pickers should block scroll/click/custom semantics actions while keeping the selected value visible and exposed as disabled semantics. - Keep repeated structural semantics configuration grouped in
PickerSemanticsor the component-specific semantics option objects instead of expanding composable signatures with more label/action parameters. - Keep repeated custom item-list configuration grouped in component-specific item option objects instead of adding separate list parameters to picker composables.
- When public picker APIs accept custom item-list option objects, validate non-empty lists, duplicate values, supported value ranges, and presence of the current selected value before composing the underlying
Picker. - When app-driven selection can come from restored values or presets, prefer public
items.coerce*helpers orstate.select*(value, items)overloads over asking apps to duplicate picker constraint logic. - When custom item lists affect first-render validity, provide
remember*State(items = ..., initial... = ...)examples so apps can normalize initial state before the picker composes. - Use
TimePickerConstraints/PickerDefaults.timePickerItems(minTime = ..., maxTime = ...)for exact inclusive time bounds. Ensure examples create state with the sameitemsobject so restored or preset times are normalized before composition. - Keep value formatting (
PickerItemFormat/ componentformat) responsible for visible item text and optional value content descriptions. KeepPickerSemanticsfor structural picker labels and action labels. When localized visible labels and TalkBack output should differ, update format examples and semantics examples together. - For
DatePickerItems.dayItems, remember that the list is filtered by the selected year/month maximum day. If a year/month change makes the current day unavailable, keep the component behavior predictable by moving to the closest available valid day and documenting that behavior. - For exact
DatePickermin/max date use cases, preferDatePickerConstraints/PickerDefaults.datePickerItems(minDate = ..., maxDate = ...)over asking apps to approximate ranges with independent year/month/day lists. Keepcontains,coerceDate, validation, and rendered column filtering aligned. - For exact
YearMonthPickermin/max use cases, preferYearMonthPickerConstraints/PickerDefaults.yearMonthPickerItems(minYearMonth = ..., maxYearMonth = ...). Keepcontains,coerceYearMonth, validation, and rendered column filtering aligned by wholeYearMonth, not independent year/month fields. - When public validation rules change, update KDoc plus
README.mdandREADME_KO.mdin the same PR, including failure mode and whether the app must clamp state before rendering. - Do not reintroduce legacy
startTime,startLocalDate, or genericstartIndexcomponent parameters. Prefer explicitremember*Stateoverloads for initial values and controlledselectedItemfor genericPicker<T>. - Treat
remember*Stateinitial parameters as first-composition defaults. Avoid resetting picker state from changing clock/date expressions during recomposition unless the API explicitly models a reset, and keep internal picker scroll state/effects keyed with state resets. - When adding public state mutation APIs, keep logical state and picker scroll position synchronized, and require apps to keep custom item lists consistent with requested values.
- During programmatic selection sync, do not let intermediate
LazyListStatescroll positions overwrite the requested state value before the picker settles. - When higher-level components pass semantics labels to
Picker, expose them through component-specific semantics option objects with sensible defaults so Android apps can localize TalkBack output. Update KDoc and both READMEs in the same PR. - Kotlin 2.2.21 ABI validation uses
checkLegacyAbi/updateLegacyAbiin this repo. Re-check task names and dump format after Kotlin upgrades. - Treat
datetimepicker/api/as committed release-gate data. Public API changes must include reviewed ABI dump updates, and reviewers should separate intended picker/state API changes from preview/generated resource churn. - Keep
@Previewcomposables private tooling code so sample previews do not become part of the supported public API surface. Reject ABI dump changes that add*Previewsymbols back todatetimepicker/api/. If accidental preview symbols are removed from ABI dumps, call out the compatibility impact in the PR and release notes. - Distinguish the latest public Maven Central/GitHub Release version from the repository
VERSION_NAME. Before changing README install snippets, verify the public release and do not point copy-paste dependency examples at unpublished versions unless the docs explicitly mark them as unreleased/local-publish usage. - When README install snippets use the latest public release but Usage/API Reference examples target
mainor unreleased APIs, add visible release-status notes near Installation, Usage, and API Reference, including thepublishToMavenLocalcommand for local testing. - Keep repository guidance up to date in this
AGENTS.mdwhen the maintainer gives durable process feedback. - Do not include local agent/tooling folders such as
.agents/or.claude/in product PRs unless the change is explicitly about agent workflow assets.
# Build library module only
./gradlew :datetimepicker:assemble --no-daemon
# Build all targets (Android, iOS, Desktop, JS)
./gradlew build --no-daemon
# Compile specific targets
./gradlew :datetimepicker:compileKotlinMetadata --no-daemon # Common
./gradlew :datetimepicker:compileKotlinDesktop --no-daemon # Desktop
./gradlew :datetimepicker:compileDebugKotlinAndroid --no-daemon# Run all tests
./gradlew :datetimepicker:test --no-daemon
# Run library Android component UI tests on Robolectric
./gradlew :datetimepicker:testDebugUnitTest --no-daemon
./gradlew :datetimepicker:testReleaseUnitTest --no-daemon
# Compile sample Android instrumented tests
./gradlew :sample:assembleDebugAndroidTest --no-daemon
# Run sample Android instrumented tests on the Gradle Managed Device target used by CI
./gradlew :sample:pixel2Api35DebugAndroidTest \
-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect \
--no-daemon
# Run sample Android instrumented tests on an already connected local device or emulator
./gradlew :sample:connectedDebugAndroidTest --no-daemon
# Run checks (tests + lint)
./gradlew :datetimepicker:check --no-daemon
# Check PR diff whitespace/conflict markers (same hygiene gate as CI)
git diff --check origin/main...HEAD
# Check public Kotlin ABI against the committed reference dump.
# Run this explicitly; do not assume :datetimepicker:check covers the ABI gate.
./gradlew :datetimepicker:checkLegacyAbi --no-daemon
# Update the Kotlin ABI reference dump after an intentional public API change
./gradlew :datetimepicker:updateLegacyAbi --no-daemon
# Verify sample app compilation
./gradlew :sample:compileKotlinDesktop --no-daemon
./gradlew :sample:wasmJsBrowserDistribution --no-daemonRun a single test: Use --tests filter:
./gradlew :datetimepicker:testDebugUnitTest --tests "com.kez.picker.TimePickerStateTest.timePickerState_24HourFormat_initialValues_areCorrect" --no-daemon# Run sample on Desktop
./gradlew :sample:desktopRun
# Install sample on Android
./gradlew :sample:installDebug
adb shell am start -n com.kez.picker.sample/.MainActivity
# iOS: open iosApp/iosApp.xcodeproj in Xcode# Check version
./gradlew printVersion
# Test local publish
./gradlew :datetimepicker:publishToMavenLocal
# Production deploy: manually triggered by GitHub Actions workflow_dispatch
# See .github/workflows/maven-central-deploy.ymlThe Picker uses a bounded cyclic buffer for infinite mode:
private const val INFINITE_SCROLL_MULTIPLIER = 1000
val listScrollCount = adjustedItems.size * INFINITE_SCROLL_MULTIPLIER
fun getItem(index: Int) = adjustedItems[index.mod(adjustedItems.size)]This keeps the picker virtually cyclic without exposing Int.MAX_VALUE list sizes.
Uses rememberSnapFlingBehavior(lazyListState) to snap items to the center position. The selected item is determined by:
listState.firstVisibleItemIndex + visibleItemsMiddleThe Picker interpolates font size and color based on item offset from center:
val fraction = abs(offset / itemHeight).coerceIn(0f, 1f)
fontSize = lerp(selectedTextStyle.fontSize, textStyle.fontSize, fraction)
color = lerp(selectedTextStyle.color, textStyle.color, fraction)Current coverage (estimated):
- Unit tests: picker states, date validation, picker index utility behavior, and date/time coercion behavior
- Robolectric Android component UI tests: picker accessibility semantics for selected values, custom labels, state descriptions, state restoration, and higher-level picker forwarding
- Sample Android instrumented smoke tests: home-screen example entry points and a representative navigation path
- Missing: broader UI interaction tests, screenshot tests, and full TalkBack/readout validation
When adding tests:
- Unit tests →
datetimepicker/src/commonTest/kotlin/ - Library Android component UI tests that do not need a real Activity/app journey →
datetimepicker/src/androidUnitTest/kotlin/with Robolectric - Library Android instrumented tests that must run on a device/emulator →
datetimepicker/src/androidInstrumentedTest/kotlin/ - Sample Android smoke tests →
sample/src/androidInstrumentedTest/kotlin/ - Use
:datetimepicker:testDebugUnitTestor:datetimepicker:testReleaseUnitTestfor library Robolectric component UI tests. Use:sample:assembleDebugAndroidTestto verify sample Android test APK compilation/packaging. Use:sample:pixel2Api35DebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirectfor the Gradle Managed Device path used by CI; it requires Android Emulator, the API 35 AOSP ATD system image for the host architecture, and local virtualization/KVM. Use:sample:connectedDebugAndroidTestwhen a local device or emulator is already available. If managed-device prerequisites are unavailable locally, runassembleDebugAndroidTestor a managed-device--dry-runand rely on CI for the actual emulator run. - Public Kotlin API/ABI changes → run
:datetimepicker:checkLegacyAbi. If the API change is intentional and SemVer-appropriate, run:datetimepicker:updateLegacyAbi, commit the updateddatetimepicker/api/dumps, and review preview/generated resource changes separately from supported picker/state API changes. - Follow naming:
<ComponentName>Test.kt,<ComponentName>RobolectricTest.kt, or<ComponentName>AndroidTest.kt
- Kotlin official style:
kotlin.code.style=official - Naming: PascalCase for composables (
TimePicker), camelCase for functions (rememberTimePickerState) - Documentation: KDoc for all public APIs with
@paramdescriptions - Formatting: Handled by Kotlin plugin (no explicit ktlint/detekt config found)
Version is set in gradle.properties:
VERSION_NAME=0.6.0Follow Semantic Versioning: MAJOR.MINOR.PATCH
GitHub Actions workflows:
integration-build-test.yml: Runs repository hygiene and multiplatform library checks on pull requests tomain; the hygiene job runsgit diff --checkon the PR diff, the dedicated macOS ABI job runscheckLegacyAbi, and the Android matrix runs the Gradle Managed Devicepixel2Api35DebugAndroidTestgate for library and sample instrumented tests.maven-central-deploy.yml: Publishes releases to Maven Central
Build matrix: Ubuntu latest for Android/Desktop/Wasm and macOS 14 for iOS, using JDK 17 (Temurin)