Skip to content

日期时间 (kotlinx-datetime)

源:kotlinx-datetime

在 Kotlin 多平台 (KMP) 生态中,kotlinx-datetime 是处理时间数据的标准方案。它不仅抹平了平台差异,还通过严谨的类型系统区分了“绝对时刻”与“本地挂钟时间”,有效规避了传统时间 API 中常见的逻辑错误。

依赖配置与版本

查看 Maven Central 最新版本

kotlin
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
groovy
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.6.0'

核心组件架构

该库的设计核心在于区分物理时间与日历时间。

  • 物理时间 (Physical Time):对应 Instant。它是时间轴上一个与时区无关的绝对点。
  • 本地时间 (Local Time):对应 LocalDateTimeLocalDate。它是特定时区下人们看到的挂钟时间。
  • 时区 (TimeZone):连接上述两者的桥梁。

时钟与当前时间获取

为了保证代码的可测试性,严禁直接在业务逻辑中使用硬编码的系统时间。

kotlin
// 获取当前的绝对时刻 (UTC)
val now: Instant = Clock.System.now()

// 将当前时刻转换为特定时区的挂钟时间
val shanghaiZone = TimeZone.of("Asia/Shanghai")
val localDateTime = now.toLocalDateTime(shanghaiZone)

println("当前时刻: $now")
println("上海时间: $localDateTime")
kotlin
// 快速获取当前日期
val today: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault())

时间算术运算深度实战

时间计算分为“基于持续时间”和“基于日历周期”两种模型。

1. 基于 Duration 的物理计算

Duration 代表精确的物理时间跨度(纳秒精度),不受日历规则影响。

kotlin
val start = Instant.parse("2024-01-01T00:00:00Z")
// 增加 24 小时 (物理上的 86400 秒)
val nextDay = start + 24.hours 

// 计算两个时刻之间的物理间隔
val duration = now - start
println("已过去 ${duration.inWholeDays} 天")

2. 基于 Period 的日历计算

DateTimePeriodDatePeriod 用于执行符合人类直觉的日历计算(如“加一个月”)。这种计算必须在特定时区的语境下进行。

kotlin
val jan31 = LocalDate(2024, 1, 31)
// 增加一个月:库会自动处理 1月31日 -> 2月29日 (闰年) 的逻辑
val feb29 = jan31 + DatePeriod(months = 1)
kotlin
val zone = TimeZone.of("Europe/Berlin")
val dateTime = LocalDateTime(2024, 3, 30, 10, 0)
val instant = dateTime.toInstant(zone)

// 增加一天的日历周期 (会自动处理夏令时切换导致的 23 或 25 小时问题)
val nextDayInstant = instant.plus(1, DateTimeUnit.DAY, zone)

比较、区间与范围判定

kotlin
val deadline = Instant.parse("2024-12-31T23:59:59Z")
val now = Clock.System.now()

// 1. 基础比较
if (now > deadline) {
    println("任务已过期")
}

// 2. 差值计算
val daysRemaining = now.until(deadline, DateTimeUnit.DAY, TimeZone.UTC)
println("剩余 $daysRemaining 天")

解析与格式化细节

kotlinx-datetime 严格遵循 ISO-8601 标准。

kotlin
// 包含时区的完整时刻
val instant = Instant.parse("2024-05-20T10:00:00Z")

// 纯日期解析
val date = LocalDate.parse("2024-05-20")
kotlin
// 目前 KMP 库本身不内置 DateTimeFormatter,需桥接平台 API
fun LocalDateTime.format(pattern: String): String {
    return this.toJavaLocalDateTime()
        .format(java.time.format.DateTimeFormatter.ofPattern(pattern))
}

单元测试中的时间模拟

通过自定义 Clock 接口,可以轻松模拟过去或未来的时间,而无需修改系统时钟。

kotlin
class FakeClock(var fixedInstant: Instant) : Clock {
    override fun now(): Instant = fixedInstant
}

@Test
fun testFeature() {
    val fakeClock = FakeClock(Instant.parse("2020-01-01T00:00:00Z"))
    val service = MyService(fakeClock)
    
    // 此时 service 内部获取到的“当前时间”永远是 2020年元旦
    assertEquals(2020, service.getCurrentYear())
}

核心工程准则

  1. 绝对时刻存 Instant:数据库存储和网络 API 交互应统一使用 UTC 的 Instant(ISO 字符串或毫秒戳)。
  2. 显示时转 LocalDateTime:仅在最外层 UI 展示时,根据用户当前设备的时区将 Instant 转换为 LocalDateTime
  3. 区分运算语义:如果是计算“30天后”,使用 plus(30.days);如果是计算“下个月同一天”,使用 plus(DatePeriod(months = 1))
  4. 注入 Clock 依赖:业务逻辑类应通过构造函数接收 Clock 实例,以确保逻辑的可测试性和确定性。