Handling Date and Time Formatting in Kotlin Time: A Complete Guide to Cross-Device Consistency
Store all date-time values as ISO-8601 strings using kotlin.time.Instant.toString() and apply locale-aware formatting only at the UI boundary to guarantee identical behavior across JVM, JavaScript, Native, and WASM targets.
Handling date and time formatting in Kotlin time requires a clear separation between your domain model and presentation layer. The JetBrains/kotlin repository provides a robust multiplatform time API in kotlin.time that ensures consistency by using ISO-8601 as the canonical representation. By adhering to the patterns implemented in libraries/stdlib/src/kotlin/time/Instant.kt, you can prevent locale-related bugs when sharing temporal data between different devices and operating systems.
Use ISO-8601 as the Canonical Representation for Kotlin Time Formatting
The foundation of consistent date handling lies in the Instant class, which represents a moment in time independent of time zones or locales. According to the source code in libraries/stdlib/src/kotlin/time/Instant.kt, the toString() method delegates to an internal formatIso function that always emits a UTC-based ISO-8601 string, such as 2023-01-02T23:40:57.120Z.
This implementation spans lines 31-74 in Instant.kt and guarantees that the output format remains fixed and locale-agnostic. Because the string representation never changes based on device settings, you can safely store these values in databases, transmit them via JSON APIs, or compare them across different platforms without parsing ambiguities.
The reverse operation, Instant.parse(), uses the same ISO-8601 logic located in lines 59-86 of Instant.kt. This symmetry ensures that any string produced by toString() can be reliably reconstructed on any Kotlin-supported target.
Store and Transmit Raw Instants to Ensure Device Consistency
When persisting temporal data, always serialize the Instant directly rather than any formatted representation. The Clock.System.now() function, defined in libraries/stdlib/src/kotlin/time/Clock.kt, provides the primary source of the current moment and works uniformly across JVM, JavaScript, Native, and WASM.
import kotlin.time.Clock
import kotlin.time.Instant
// Serialize for storage or transmission
fun storeInstant(): String {
val now: Instant = Clock.System.now()
return now.toString() // ISO-8601: 2023-01-02T23:40:57.120Z
}
// Deserialize from external source
fun loadInstant(encoded: String): Instant? =
runCatching { Instant.parse(encoded) }.getOrNull()
By storing only the ISO-8601 string, you eliminate the risk of locale-specific parsing errors when the data is read on a device with different language settings. This approach is particularly critical for distributed systems where Android, iOS, and server-side JVM components exchange temporal data.
Apply Locale-Aware Formatting Only at the UI Boundary
The recommended architecture for handling date and time formatting in Kotlin time separates domain logic from presentation. Keep Instant objects in your business logic, and convert them to human-readable strings only when rendering the UI.
JVM Platform Formatting with DateTimeFormatter
On the JVM, convert kotlin.time.Instant to java.time.Instant using the toJavaInstant() extension defined in libraries/stdlib/jvm/src/kotlin/time/InstantJvm.kt (lines 63-68). Then apply DateTimeFormatter with the user's locale.
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.time.Clock
import kotlin.time.toJavaInstant
fun formatForUser(): String {
val instant = Clock.System.now().toJavaInstant()
val formatter = DateTimeFormatter.ofPattern("EEEE, d MMMM yyyy HH:mm", Locale.getDefault())
.withZone(ZoneId.systemDefault())
return formatter.format(instant) // e.g., "Monday, 2 January 2023 15:40"
}
This approach leverages the full ICU locale data available on the JVM while maintaining a clean separation from the domain model.
Multiplatform Formatting with kotlinx.datetime
For common code shared across platforms, use the kotlinx.datetime library. Convert Instant to LocalDateTime using the current system time zone, then format manually or delegate to platform-specific APIs.
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
fun formattedCommon(): String {
val instant = Clock.System.now()
val local = instant.toLocalDateTime(TimeZone.currentSystemDefault())
// Manual formatting for common code; replace with platform APIs for production
return "${local.date} ${local.time.hour.toString().padStart(2, '0')}:${local.time.minute.toString().padStart(2, '0')}"
}
This strategy ensures that your domain logic remains in kotlin.time while presentation concerns are handled appropriately for each target platform.
JavaScript and Native Platform Handling
On JavaScript, delegate to Intl.DateTimeFormat for locale-aware formatting. On Native platforms (iOS), use NSDateFormatter or equivalent. Always convert from the ISO-8601 string or Instant object at the boundary rather than storing formatted strings.
Summary
- Store canonical ISO-8601: Use
Instant.toString()andInstant.parse()fromlibraries/stdlib/src/kotlin/time/Instant.ktfor all persistence and transmission. - Separate domain from presentation: Keep
Instantobjects in business logic; apply formatting only when rendering UI. - Leverage platform formatters: Use
DateTimeFormatteron JVM,Intl.DateTimeFormaton JS, and native APIs on iOS/Android, converting viatoJavaInstant()orkotlinx.datetimeextensions. - Avoid locale-dependent storage: Never persist human-readable date strings to databases or APIs; always use the fixed ISO format implemented in
formatIso.
Frequently Asked Questions
Why should I use ISO-8601 for storing dates in Kotlin?
ISO-8601 provides a fixed, unambiguous text representation that does not change based on device locale or time zone settings. The formatIso function in libraries/stdlib/src/kotlin/time/Instant.kt guarantees that Instant.toString() always outputs UTC-based ISO-8601 (e.g., 2023-01-02T23:40:57.120Z), ensuring that data serialized on an Android device in Japan can be correctly parsed on a JVM server in Brazil without formatting errors.
How do I convert kotlin.time.Instant to Java's Instant?
Use the toJavaInstant() extension function defined in libraries/stdlib/jvm/src/kotlin/time/InstantJvm.kt (lines 63-68). This zero-cost conversion allows you to pass Kotlin Instant objects to Java DateTimeFormatter or other legacy APIs while maintaining the same underlying epoch timestamp. Remember to import kotlin.time.toJavaInstant to access the extension.
What is the difference between kotlin.time and kotlinx.datetime?
kotlin.time (in the standard library) provides the fundamental Instant, Duration, and Clock types for measuring moments and elapsed time. kotlinx.datetime is a separate multiplatform library that builds on kotlin.time to add LocalDateTime, LocalDate, TimeZone, and conversion utilities like Instant.toLocalDateTime(). For formatting, you typically use kotlin.time for domain logic and kotlinx.datetime (or platform-specific APIs) for presentation logic.
How do I handle custom date patterns in common Kotlin multiplatform code?
Since kotlinx.datetime does not yet provide a pattern-based formatter for common code, you should convert the Instant to a LocalDateTime using toLocalDateTime(TimeZone.currentSystemDefault()), then manually construct the string or use expect/actual declarations. The actual implementations delegate to DateTimeFormatter on JVM, Intl.DateTimeFormat on JavaScript, and NSDateFormatter on Native. This approach keeps your domain model clean while allowing rich, locale-aware formatting at the platform boundary.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →