12 Minutes
How to Use Kotlin Date & Time: Formatting, Strings & More
Fix Bugs Faster! Log Collection Made Easy
Kotlin Date and Time Explained (LocalDate, Instant, Formats)
Choosing the wrong date-time API can seriously snarl up your Kotlin app. Timezone mismatches, formatting bugs, inconsistent timestamps – all of them can seriously drain your time and they’re hard to trace without the right tooling.
Kotlin gives you multiple date-time tools – LocalDate, Instant, DateTimeFormatter, and kotlinx-datetime – but each is designed for a specific use case across Android, server-side, and multiplatform projects. Pick the right one and your date handling is predictable and easy to debug: pick the wrong one and you’re chasing phantom bugs at 2am.
This guide is designed to help you make the right call. Here’s what’s on the agenda:
Table of Contents
- Kotlin Date and Time Explained (LocalDate, Instant, Formats)
- Creating dates in Kotlin (LocalDate, LocalDateTime)
- Formatting dates (patterns, formatters, localized formats)
- Getting the current date & time in Kotlin (now(), Time APIs)
- Working with timestamps (Epoch ↔ Date)
- Date arithmetic (period, duration, adding/subtracting dates)
- Moving between time zones with Instant, ZonedDateTime and conversions
- Using kotlinx-datetime (Multiplatform API)
- Summary
Creating dates in Kotlin (LocalDate, LocalDateTime)
If you want a really simple way to create date values across time zones or platform differences, Kotlin’s LocalDate and LocalDateTime APIs are a great starting-point. They let you define dates from components, parse inputs and extract specific fields, and they’re trained to handle most of the common scenarios you’ll run into.
Let’s go step by step:
Create dates from components
We’d advise usingLocalDate.of() or LocalDateTime.of() when you have explicit year, month, and day values are available. This is the most fail-safe way to create dates for events, deadlines, or configuration values because it removes parsing ambiguity.
val date = LocalDate.of(2018, 12, 31)
If hours and minutes are needed, LocalDateTime.of() provides the same clarity.
Parse strings into dates
When your app receives date inputs as text – like API responses, form fields, or stored values – LocalDate.parse() converts ISO-format strings straight into LocalDate objects. This is about as simple as parsing gets, and it leaves a clean trace if something goes wrong down the track.
val date = LocalDate.parse("2018-12-12")
Extract year, month and day
Once you have a LocalDate, accessing components like the year, month, or day is straightforward using Kotlin’s built-in properties. These values are useful for filtering, grouping, or formatting dates.
val year = date.year
val month = date.month
val day = date.dayOfMonth
Formatting dates (patterns, formatters, localized formats)
Kotlin supports a wide range of date format patterns for global usage. Examples include:
"yyyy-MM-dd"→2023-12-31"dd/MM/yyyy"→31/12/2023"MMM d, yyyy"→Dec 31, 2023"EEE, MMM d"→Sun, Dec 31
Generally, we’ve found that good date formatting is a balance between consistency and local expectations. That means deciding:
- Text vs numeric months and weekdays
- Full vs short names
- Separators (slashes, dots, dashes)
- Whether to include weekday or time
Using consistent patterns is essential when our app is used across multiple regions or devices. Inconsistency can lead to bugs that don’t throw exceptions: they just silently confuse users. We’ve seen it surface in Bugfender logs, with reported complaints that don’t match any visible error. Nailing the formatting from the start saves that investigation time.
This probably seems like a hassle if you’re not familiar. But this is where DateTimeFormatter comes into its own.
Customizing dates with DateTimeFormatter
DateTimeFormatter is Kotlin’s go-to class for converting date objects into readable text. Whether we need ISO formats, localized outputs or user-friendly patterns, DateTimeFormatter lets us use predefined constants for standard formats or specify patterns to meet our app’s specific UX or backend requirements. And it works in both direcitons:
- On one hand, we can convert a
LocalDateinto a text string like"2025-03-15”. - Or we can go the other way and parsing a text string into a date-time object.
Converting a LocalDate into a text string
DateTimeFormatter and the .format() function will convert today’s date into a clean, predictable string. This is the pattern you want when sending dates to an API, preparing UI labels, or generating log entries — the kind of timestamps that make logs immediately readable when you’re tracing an issue across devices.
This code is reusable and great for making quick adjustments:
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val formattedDate = LocalDate.now().format(formatter)
While this alternative is more concise and less noisy.
val stringDate = LocalDate.now()
.format(DateTimeFormatter.ofPattern("MMM d, yyyy"))
Alternatively, if you want to format a specific date (rather than today’s), here’s the code to use:
val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
val formatted = LocalDate.of(2025, 12, 31).format(formatter)
Parsing strings into dates
This enables us to display a date that you and your users can work with. The code is very simple here, replacing of with parse in the second line:
val formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy")
val date = LocalDate.parse("31-12-2025", formatter)
A quick note on ISO 8601 & W3C date-time formats
Many APIs, databases, and cross-platform systems rely on ISO 8601 / W3C date-time formats for consistent serialization. These formats avoid locale issues and ensure predictable parsing across Kotlin, Android, servers, and JavaScript.
Common ISO/W3C examples:
- Year only:
1997 - Year–month:
1997-07 - Full date:
1997-07-16 - Date + time (hours + minutes):
1997-07-16T19:20+01:00 - With seconds:
1997-07-16T19:20:30+01:00 - With fractional seconds:
1997-07-16T19:20:30.45+01:00
These formats are ideal for APIs, logs, time-stamped analytics and any code that needs reliable cross-platform behavior.
Getting the current date & time in Kotlin (now(), Time APIs)
Kotlin gives you a whole bunch of ways to retrieve the current date and time, each suited to a different level of precision. Choosing the right one matters more than it might seem: we’ve seen apps where mixing LocalDateTime and Instant inconsistently leads to timestamp drift that only shows up under specific timezone conditions (this kind of bug is nearly invisible without real-device log data).
LocalDate.now()
LocalDate.now() returns today’s date without time information, suitable for daily summaries, comparisons, and calendar-based logic.
val currentDate = LocalDate.now()
LocalDateTime.now()
LocalDateTime.now() provides both date and time elements, without time-zone context. This is ideal for local operations such as UI display values or lightweight logging.
val currentDateTime = LocalDateTime.now()
Instant.now()
We will dig into Instant in more detail in the next section. Essentially, it allows us to distinguish the time shown on local clocks with absolute, universal time.
Instant.now() captures the current moment in UTC, offering consistent behavior across devices and platforms. It is a reliable option for logging, analytics, synchronization, and backend communication. Basically, anywhere you need timestamps that are consistent regardless of where a device is physically located.
val currentInstant = Instant.now()
Working with timestamps (Epoch ↔ Date)
Timestamps represent the number of milliseconds elapsed since the Unix Epoch (January 1, 1970), the ‘year zero’ for all Unix-based systems. This is essentially a simple, zone-independent way of representing time.
Much like a Unix timestamp,Instant represents a moment in UTC and distinguishes civil time (the expression of time that humans interact with via clocks, calendars and alarms) from the universal chronometer.
Kotlin offers several ways to convert between timestamps and the human-readable date types we use every day, relying heavily on Instant. This makes it easy to generate logs, schedule notifications or maintain consistent server-side formats.
Convert timestamps to readable dates
To turn a timestamp into a usable date, we can convert the epoch value into an Instant, then adjust it into a LocalDate or LocalDateTime depending on our needs.
val timestamp: Long = System.currentTimeMillis()
val instant = Instant.ofEpochMilli(timestamp)
val date = instant.atZone(ZoneId.systemDefault()).toLocalDate()
This approach ensures proper timezone handling when presenting dates in the user’s local context.
Convert dates back to timestamps
We can convert a date or date-time back into a timestamp by converting it to an Instant and extracting the epoch value. This is common when storing time data or comparing events chronologically.
val currentInstant = Instant.now()
val currentTimestamp = currentInstant.toEpochMilli()
This yields a consistent, portable numeric value that works across devices and platforms.
Legacy Date and Calendar support
Some Android codebases still use Java’s Date or Calendar APIs, especially when working with older libraries or existing code. Kotlin interoperates cleanly with these classes when needed.
val currentLegacyDate = Date()
val legacyTimestamp = currentLegacyDate.time
Although still supported, the modern java.time APIs (Instant, LocalDate, LocalDateTime) are generally more predictable and easier to maintain.
Date arithmetic (period, duration, adding/subtracting dates)
We need date arithmetic whenever our software has to perform functions or calculations related to the passage of time. Just some examples:
- To notify users of upcoming appointments a set period of time in advance.
- To repeat a notification at a fixed recurring time.
- To calculate expiry dates for promotions or memberships.
- To handle billing cycles and invoicing periods.
Kotlin provides several tools for adjusting dates and calculating differences, primarily through Period, Duration, and the arithmetic functions available on LocalDate and LocalDateTime. Here’s what you need to know.
Add or subtract days, months and years
LocalDate includes built-in functions such as .plusDays(), .minusMonths(), and .plusYears() that make date manipulation straightforward and expressive.
val adjustedDate = LocalDate.now()
.minusDays(5)
.plusWeeks(3)
.withDayOfMonth(15)
These operations are particularly useful when generating reminders, adjusting future dates, or recalculating user-specific intervals.
Calculate the difference between two dates
To compare how far apart two moments are, Kotlin offers Period for date-based differences and Duration for time-based differences.
Period is ideal for broader date-based calculations like the number of days between two events. For precise time differences—including hours, minutes, and seconds—Duration is the better option.
val earlierDate = LocalDate.of(2023, 1, 10)
val laterDate = LocalDate.of(2023, 1, 20)
val gap = Period.between(earlierDate, laterDate)
val daysDifference = gap.days
Use date ranges
Natural date range checks allow our apps to determine whether a given date falls within a particular period, such as a promotion window, subscription cycle, or event timeframe.
Kotlin supports natural date range checks using the .. operator, as the following string demonstrates.
val start = LocalDate.of(2022, 9, 1)
val end = LocalDate.of(2022, 9, 30)
val dateRange = start..end
if (LocalDate.now() in dateRange) {
println("Valid date range")
}
Moving between time zones with Instant, ZonedDateTime and conversions
Given that more than 35 million commercial flights take off every single year, it’s highly likely that some of your users will move between time zones. And honestly, timezone handling is one of the most common roots of subtle, hard-to-reproduce bugs we see reported through Bugfender.
Users moving between zones, devices that haven’t updated their timezone settings, or server timestamps being displayed without conversion — these issues tend to appear as confusing date displays rather than outright errors, which makes them hard to catch before they hit users.
Thankfully Kotlin’s java.time API provides clear tools for converting timestamps, adjusting dates and ensuring consistent experiences across locales.
Convert ZonedDateTime to LocalDateTime
Sometimes time-zone details are irrelevant: only the local date and time fields are needed, without any timezone offset. In this case, ZonedDateTime can be reduced to LocalDateTime.
val zoned = ZonedDateTime.now()
val localDateTime = zoned.toLocalDateTime()
This is particularly useful for calendar displays, form inputs, or internal calculations where only the civil time matters.
Convert Instant to a user-local ZonedDateTime
Returning to Instant, we can apply a ZoneId and convert it to ZonedDateTime, displaying the moment in a user’s LocalDateTime.
val instant = Instant.now()
val userZone = ZoneId.systemDefault()
val userLocalDateTime = instant.atZone(userZone)
This approach ensures the same timestamp is interpreted correctly regardless of where the device is located, and guarantees clarity when converting between universal time and local context.
We can also apply aTimeZone explicitly:
val userZone = TimeZone.currentSystemDefault()
val userLocal = instant.toLocalDateTime(userZone)
Common pitfalls with time-zone handling
These are some of the most common pitfalls we see in logs and bug reports:
- Midnight boundary shifts. Converting between time zones may push a date into the previous or next day.
- Incorrect zone assumptions. Using
ZoneId.systemDefault()in shared business logic may create inconsistencies if devices run in different locales. - Offset vs time zone. When precision matters, always use named zones like
"Europe/Paris"instead of fixed offsets like"+01:00".
Consistent handling prevents mismatches that can confuse users or break chronological ordering.
Using kotlinx-datetime (Multiplatform API)
As well as ensuring consistent behavior across time zones, it’s paramount that we ensure consistent behavior across devices. Kotlin runs on various different platforms with differing date-time engines, and older devices may be burdened with legacy APIs.
kotlinx-datetime is Kotlin’s multiplatform date-time library, designed to ensure consistent behavior across Android, JVM, iOS, and JavaScript. It provides a simplified API built around ISO-8601 and avoids many of the pitfalls found in older date-time libraries.
Adding kotlinx-datetime to your project
To use kotlinx-datetime in a Kotlin Multiplatform or Android project, be sure to include the dependency in your Gradle configuration:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:x.y.z")
}
// Replace x.y.z with the latest version
This library provides multiplatform-friendly versions of Instant, LocalDate, LocalDateTime and TimeZone, making it ideal for shared KMP code.
Core data types
The library includes several essential types. These types map closely to the concepts used in java.time, but in a multiplatform-friendly form.
val instant = Clock.System.now() // UTC moment
val date = LocalDate(2023, 11, 5) // Calendar date
val dateTime = LocalDateTime(2023, 11, 5, 15, 30) // Local date-time
val time = LocalTime(7, 0) // Local time
Parse and format ISO strings
kotlinx-datetime relies on ISO-8601 by default, making parsing predictable and reducing ambiguity in distributed systems.
val isoInstant = Instant.parse("2024-01-15T10:20:30Z")
val isoDate = LocalDate.parse("2024-01-15")
ISO formatting avoids issues with locale variations and is ideal for APIs, logs and storage.
When to use it
kotlinx-datetime is the preferred choice when:
- Building Kotlin Multiplatform applications.
- Needing predictable, cross-device behavior.
- Working with serialized timestamps in APIs.
- Seeking a lightweight and consistent alternative to
java.time.
For pure Android apps, java.time remains fully supported and often integrates better with existing platform APIs—but kotlinx-datetime is ideal when consistency across platforms matters.
Summary
Kotlin provides a modern and consistent set of tools for working with dates and times across Android, JVM, and multiplatform environments.
With LocalDate, LocalDateTime, Instant, and DateTimeFormatter, we can create dates, format them, convert timestamps, perform arithmetic, and handle time zones with precision.For multiplatform projects, kotlinx-datetime offers a unified API that behaves predictably across all platforms, while still supporting the core operations developers use most often.
Get the formatting patterns right, handle conversions safely, and be explicit about time zones and your date logic will be one less conundrum in prod. And honestly, we can’t stress the importance of accurate timestamps and consistent date handling enough.
And just one more thing: if you’re building Android or Kotlin apps and want to track real device logs, errors, and timestamps reliably, tools like Bugfender help surface client-side behavior directly from user devices. Want to know how we can help you? Let’s chat!
Expect The Unexpected!
Debug Faster With Bugfender