还在用 Date 处理日期和时间?该试试 kotlinx-datetime 了

前言

kotlinx-datetime 是 Kotlin 官方的日期和时间库。个人最早了解到此库是在封装 Kotlin 的日期和时间工具类,当时是用 Java8 的 LocalDateTime 封装扩展库。后来偶然发现 Kotlin 官方日期时间库,学习了解后觉得比 Java8 的更加简洁好用,就把原来 Java8 封装好的日期和时间工具类换一套实现方式推倒重做了。下面会带大家来一起来学习 kotlinx-datetime 库。

本文好像是国内第一篇讲解 kotlinx-datetime 的文章,并且可能是全网讲得最深入的文章。不仅仅会介绍该库怎么使用,还会带大家来封装优化 kotlinx-datetime,使其更加完善和好用。其中还会讲到一个多数人很难发现的注意事项。最后还会用个微信时间的例子实战一下。

Java 与 Kotlin 库的 API 区别

学习 kotlinx-datetime 之前,我们有必要先了解下 Java7、Java8 和 Kotlin 库的日期和时间 API 有什么区别。

Java7 与 Java8

大多数人还在使用 Java7 的日期时间 API,其实在 Java8 引入了更好用的 API,两者有以下区别:

  • Java7 的定义在 java.util 包,Java8 的定义在是 java.time 包。
  • Java7 主要包括DateCalendarTimeZone 这几个类,Java8 主要包括 LocalDateTimeZonedDateTimeZoneId 等。
  • Java7 使用SimpleDateFormat 进行格式化,是线程不安全的。Java8 使用 DateTimeFormatter 格式化是线程安全的,可以只创建一个实例到处引用。
  • Java8 修改了不合理的常量设计,1 月到 12 月的值是 1 ~ 12,周一到周日的值是 1 ~ 7。而 Java7 的并不是,使用起来会有些别扭。
  • Java8 能很方便地调整日期和时间,或者对日期和时间进行加减运算。

比较下来当然是更推荐用 Java8 的日期时间 API。

Java8 与 Kotlin 库

kotlinx-datetime 的 API 参考了 Java8 的,两者有很多相同或相似的类或 API,不过也有些区别:

  • Java8 的定义在是 java.time 包,Kotlin 的定义在是 kotlinx.datetime 包。
  • kotlinx.datetime 的是多平台库,在 JVM 平台的实现是基于 Java8 的。由于多平台的特性,每个 API 都要考虑各个平台的情况,所以 kotlinx.datetime 并没有完全引入 Java8 的 API,目前只有常用的功能。
  • kotlinx-datetime 用了 Kotlin 语言实现,API 更加简洁好用。

两者其实很相似,各有优劣。kotlinx-datetime 功能没 Java8 的全,但是 API 会更加简洁易用,并且是多平台库。使用 Kotlin 开发的话,个人更推荐使用 kotlinx-datetime,毕竟是 Kotlin 官方的东西。不过也有点问题需要我们去解决,后面会给出一套完美的解决方案。

开始使用

准备工作

首先要增加 kotlinx-datetime 的依赖,并且配置 API 脱糖。因为要在 SDK 26 及以上才能使用到 Java8 的 API,而脱糖可以摆脱这个限制,使得低版本也能使用 Java8 的 API。

android {
    defaultConfig {
        // minSdkVersion 是 20 或更低版本时需要开启
        multiDexEnabled true
    }

    compileOptions {
        // 启用对新语言 API 的支持
        coreLibraryDesugaringEnabled true
        // Java 8
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
    implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
}
复制代码

类型

Instant

表示某一时刻,可通过时间戳来创建对象。

val instant = Instant.fromEpochSeconds(164367360)
// val instant = Instant.fromEpochMilliseconds(164367360000)
复制代码

Instant 保存了以秒为单位的时间戳和更精确的纳秒精度。

val second = instant.epochSecond
val nano = instant.nanosecondsOfSecond
复制代码

LocalDateTime

表示一个本地日期和时间,保存了年月日时分秒纳秒的信息。

val localDateTime = LocalDateTime(2022, 2, 1, 8, 0, 0)
val year = localDateTime.year
val hour = localDateTime.hour
复制代码

LocalDate

表示一个本地日期,保存了年月日信息。

val springFestival = LocalDate(2022, 2, 1)
val year = springFestival.year
val month = springFestival.month.value
val dayOfMonth = springFestival.dayOfMonth
复制代码

LocalDateLocalDateTime 的日期组成部分。

val localDate = localDateTime.date
复制代码

想用 LocalDate 转成 LocalDateTime 需要补充时间信息。

val localDateTime = localDate.atTime(10, 8)
复制代码

Clock

表示时钟,可以理解为生活中的电子时钟。主要用于查看当前的时刻和今天的日期。

val now: Instant = Clock.System.now()
val timeZone: TimeZone = TimeZone.currentSystemDefault()
val today: LocalDate = Clock.System.todayAt(timeZone)
复制代码

DayOfWeek/Month

表示星期和月份,可通过 LocalDateTimeLocalDate 对象获取,或者直接使用 DayOfWeek.MONDAYMonth.JANUARY 等枚举变量。

val localDate = LocalDate(2022, 2, 1)

val dayOfWeek: DayOfWeek = localDate.dayOfWeek
dayOfWeek.getDisplayName(TextStyle.FULL, Locale.CHINA)     // 星期二
dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.CHINA)    // 周二
dayOfWeek.getDisplayName(TextStyle.NARROW, Locale.CHINA)   // 二
dayOfWeek.getDisplayName(TextStyle.FULL, Locale.ENGLISH)   // Tuesday
dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.ENGLISH)  // Tue
dayOfWeek.getDisplayName(TextStyle.NARROW, Locale.ENGLISH) // T

val month: Month = localDate.month
month.getDisplayName(TextStyle.FULL, Locale.CHINA)     // 二月
month.getDisplayName(TextStyle.SHORT, Locale.CHINA)    // 2月
month.getDisplayName(TextStyle.NARROW, Locale.CHINA)   // 2
month.getDisplayName(TextStyle.FULL, Locale.ENGLISH)   // February
month.getDisplayName(TextStyle.SHORT, Locale.ENGLISH)  // Feb
month.getDisplayName(TextStyle.NARROW, Locale.ENGLISH) // F
复制代码

之前用这个画星期和月份的坐标轴非常爽,不用自己去计算显示什么文本。文字样式有长有短可以选择,还支持了多语言,直接符合了 UI 的需求。

用法

kotlinx-datetime 提供了很多处理日期和时间的类,但是我们平时开发主要是处理时间戳和具体的日期和时间,所以主要关注 InstantLocalDateTimeLocalDate,其它的类基本是用于辅助使用的。

以下是官方给出的在什么情况下选择使用哪种类型的一些基本建议:

  • Instant 表示过去已经发生的事件的时间戳(如聊天的时间戳),或肯定会在距现在不远的未来明确定义的时刻发生(如订单确认截止日期在 1 小时后)。

  • LocalDateTime 表示预定在很久的将来某个时间发生的事件的时间(如几个月后的预定会议)。必须单独跟踪预定事件的时区。尽量避免提前将未来的事件转换为 Instant,因为将来时区规则可能会意外更改。此外,将 Instant 转为 LocalDateTime 可用于显示
    UI 上的时间。

  • LocalDate 表示没有特定时间关联的事件日期(如出生日期)。

时间计算

Instant 计算

加减特定数量的日期、时间单位或者一个时间段:

val now = Clock.System.now()
val timeZone = TimeZone.currentSystemDefault()
val tomorrow = now.plus(2, DateTimeUnit.DAY, timeZone)
val threeYearsAndAMonthLater = now.plus(DateTimePeriod(years = 3, months = 1), timeZone)
复制代码

调用 Instant.periodUntil(Instant, TimeZone) 函数可以得到两个时刻相差的时间段。

val period: DateTimePeriod = instantInThePast.periodUntil(Clock.System.now(), TimeZone.UTC)
val hours = period.hours   // 相差了多少个小时
复制代码

也可以用 Instant.until(Instant, DateTimeUnit.TimeBased, TimeZone) 函数将差值计算为指定日期或时间单位的数量:

val diffInMonths: Int = instantInThePast.until(Clock.System.now(), DateTimeUnit.MONTH, TimeZone.UTC)
复制代码

还提供了 yearsUntil(...)monthsUntil(...)daysUntil(...) 函数直接得到两个时刻相差了几年、几个月、几天。

要注意,plus...until 操作需要 TimeZone 作为参数,因为在不同时区计算时,两个特定时刻之间的日历间隔可能不同。

LocalDate 计算

LocalDate 有着和 Instant 类似的 plus(...)until(...)periodUntil(...)yearsUntil(...)monthUntil(...)daysUntil(...) 函数。不同的是,日期的计算只能有年月日不能包含时分秒,所以参数改成了 DateTimeUnit.DateBasedDatePeriod 类型,并且不需要传时区。

LocalDateTime 计算

LocalDateTime 并没有上述的时间运算,因为夏令时转换的存在会导致计算结果有不确定的因素。使用了夏令时的地区,会在接近春季开始的时候,将时间调快一小时,并在秋季调回正常时间。

又因为 LocalDateTime 表示与时区无关的日期和时间,可能出现加了 1 天却不是增加 24 小时的情况,所以不能直接用 LocalDateTime 计算。需要先转成 Instant 进行计算,然后再转回 LocalDateTime 获取某时区的日期和时间,这样才不会出错。

val timeZone = TimeZone.of("Europe/Berlin")
val localDateTime = LocalDateTime.parse("2021-03-27T02:16:20")
val instant = localDateTime.toInstant(timeZone)

val instantOneDayLater = instant.plus(1, DateTimeUnit.DAY, timeZone)
val localDateTimeOneDayLater = instantOneDayLater.toLocalDateTime(timeZone)
// 2021-03-28T03:16:20, 当天的 02:16:20 是在同一时段

val instantTwoDaysLater = instant.plus(2, DateTimeUnit.DAY, timeZone)
val localDateTimeTwoDaysLater = instantTwoDaysLater.toLocalDateTime(timeZone)
// 2021-03-29T02:16:20
复制代码

时间比较

由于 Kotlin 有操作符重载的语法特性,所以 InstantLocalDateTimeLocalDate 能直接比大小得出时间的早晚。

if (instant1 < instant2) {
  // ...
}
复制代码
if (localDateTime1 >= localDateTime2) {
  // ...
}
复制代码

时间转换

在不同时区的同一时刻得到的日期和时间是不一样的,所以 InstantLocalDateTime 互转会有个时区参数。

val instant = Clock.System.now()
val timeZone = TimeZone.currentSystemDefault()
val localDateTime = instant.toLocalDateTime(timeZone)
复制代码
val localDateTime = LocalDateTime(2022, 2, 1, 8, 0)
val timeZone = TimeZone.currentSystemDefault()
val instant = localDateTime.toInstant(timeZone)
复制代码

我们开发最常用的是时间戳和字符串的转换,而 Instant 保存了时间戳,所以我们关心 InstantLocalDateTime 和字符串互转。

可是目前 InstantLocalDateTimeLocalDate 仅支持 ISO-8601 格式。所以没有 format() 函数,只有调用 toString() 函数转换为对应格式的字符串。

instant.toString()        // 2022-02-01T12:30:00.048Z
localDateTime.toString()  // 2022-02-01T12:30:00
localDate.toString()      // 2022-02-01
复制代码

同理只支持 ISO-8601 格式的字符串转为 InstantLocalDateTimeLocalDate

"2022-02-01T12:30:00.048Z".toInstant()
"2022-02-01T12:30:00".toLocalDateTime()
"2022-02-01".toLocalDate()
复制代码

估计因为 kotlinx-datetime 是一个多平台的时间日期库,虽然 Java 平台实现起来不难,但是在别的平台需要用别的平台的工具,并没有那么容易实现。

等 Kotlin 官方实现格式化功能的话不知道要等到猴年马月,个人花了一段时间研究后是发现我们是有办法自己来完善的。接下来分享一下个人摸索出来的完美的解决方案。

封装优化

完善格式化和解析的功能

首先当然是搜一下网上有没什么解决思路。由于这个库有点新,讲解的文章都很少,搜了很久只搜到了唯一一个有用的方案:

"2010-06-01 22:19:44".replace(" ", "T").toLocalDateTime()
复制代码

确实有点用,但仅仅适用于格式是 yyyy-MM-dd HH:mm:ss 的情况,格式一旦变了就没法用了。

既然网上没有好的解决方案,那就自己研究吧。深入了解源码得知 kotlinx-datetime 的 API 在 JVM 都是代理给对应的 Java8 对象。比如 LocalDateTime 的源码:

import java.time.LocalDateTime as jtLocalDateTime

@Serializable(with = LocalDateTimeIso8601Serializer::class)
public actual class LocalDateTime internal constructor(internal val value: jtLocalDateTime) : Comparable<LocalDateTime> {

  public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) :
          this(try {
            jtLocalDateTime.of(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond)
          } catch (e: DateTimeException) {
            throw IllegalArgumentException(e)
          })
  
  public actual val year: Int get() = value.year
  public actual val monthNumber: Int get() = value.monthValue
  public actual val month: Month get() = value.month
  public actual val dayOfMonth: Int get() = value.dayOfMonth
  public actual val dayOfWeek: DayOfWeek get() = value.dayOfWeek
  public actual val dayOfYear: Int get() = value.dayOfYear

  public actual val hour: Int get() = value.hour
  public actual val minute: Int get() = value.minute
  public actual val second: Int get() = value.second
  public actual val nanosecond: Int get() = value.nano
  
  // 省略部分代码
}
复制代码

jtLocalDateTime 是 Java8 的 java.time.LocalDateTime 的别名,该对象在构造函数传入。Kotlin 的 LocalDateTime 功能都是代理给 Java8 对象实现。

我们可以同理用 Java8 的对象实现格式化功能,再转为 Kotlin 的对象。但问题是 Java8 对象用了 internal 修饰不对外公开,自己手动实现一套格式化时间的功能也不现实,当时思路一下就卡住了。后面思考了很久想到了一个曲线救国的办法,既然不能直接拿到 java8 对象,那我们就自己手动创建一个吧。

来实践一下,我们先来完善 String.toLocalDateTime() 函数,想在转化的时候支持任意格式,那肯定要多传一个参数,优化成下面的用法。

"2022年02月01日 22:19:44".toLocalDateTime("yyyy年MM月dd日 HH:mm:ss")
复制代码

这就可以增加一个扩展函数来实现。先通过 Java8 的 LocalDateTime 解析时间,然后再用年月日时分秒去创建 Kotlin 的 LocalDateTime

fun String.toLocalDateTime(pattern: String): LocalDateTime =
  java.time.LocalDateTime.parse(this, DateTimeFormatter.ofPattern(pattern))
    .run { LocalDateTime(year, month, dayOfMonth, hour, minute, second, nano) }
复制代码

测试一下真的得到我们想要的结果了!!可以用这个思路去实现其它的格式化和解析功能。

后来发现这个思路稍微有点点瑕疵,就是会重复创建 Java8 对象,我们手动创建一次后,在构造函数又会创建一次,总觉得还差点意思。

有些完美主义的我再去翻了一遍库的源码,确定是不是只能这么做,没想到在 jvm
的模块发现一个 Converters.kt 文件,包含了各种 Kotlin 和 Java8
的时间日期类互转的扩展,原来是要通过扩展方法得到 internal 修饰的 Java8 对象,这也藏得太隐蔽了吧…

/**
 * Converts this [kotlinx.datetime.LocalDateTime][LocalDateTime] value to a [java.time.LocalDateTime][java.time.LocalDateTime] value.
 */
public fun LocalDateTime.toJavaLocalDateTime(): java.time.LocalDateTime = this.value

/**
 * Converts this [java.time.LocalDateTime][java.time.LocalDateTime] value to a [kotlinx.datetime.LocalDateTime][LocalDateTime] value.
 */
public fun java.time.LocalDateTime.toKotlinLocalDateTime(): LocalDateTime = LocalDateTime(this)
复制代码

我们再优化一下,把前面用年月日时分秒去创建 Kotlin 的 LocalDateTime 代码,改为调用官方的 toKotlinLocalDateTime() 函数。

fun String.toLocalDateTime(pattern: String): LocalDateTime =
  java.time.LocalDateTime.parse(this, DateTimeFormatter.ofPattern(pattern)).toKotlinLocalDateTime()
复制代码

这样就不会重复创建 Java8 对象啦,完美地解决了格式化的问题。

我们还可以用这个思路补充更多的格式化和解析功能,方便日常使用。

fun Instant.Companion.parse(text: String, pattern: String, timeZone: TimeZone): Instant =
  java.time.ZonedDateTime.parse(text, DateTimeFormatter.ofPattern(pattern).withZone(timeZone.toJavaZoneId())).toInstant().toKotlinInstant()

fun LocalDateTime.Companion.parse(text: String, pattern: String): LocalDateTime =
  java.time.LocalDateTime.parse(text, DateTimeFormatter.ofPattern(pattern)).toKotlinLocalDateTime()

fun LocalDate.Companion.parse(text: String, pattern: String): LocalDate =
  java.time.LocalDate.parse(text, DateTimeFormatter.ofPattern(pattern)).toKotlinLocalDate()

fun String.toInstant(pattern: String, timeZone: TimeZone): Instant = Instant.parse(this, pattern, timeZone)

fun String.toLocalDateTime(pattern: String): LocalDateTime = LocalDateTime.parse(this, pattern)

fun String.toLocalDate(pattern: String): LocalDate = LocalDate.parse(this, pattern)

fun Instant.format(pattern: String, timeZone: TimeZone): String =
  DateTimeFormatter.ofPattern(pattern).withZone(timeZone.toJavaZoneId()).format(toJavaInstant())

fun LocalDate.format(pattern: String): String =
  DateTimeFormatter.ofPattern(pattern).format(toJavaLocalDate())

fun LocalDateTime.format(pattern: String): String =
  DateTimeFormatter.ofPattern(pattern).format(toJavaLocalDateTime())
复制代码

简化默认时区参数

kotlinx-datetime 还有个使用起来略显繁琐的地方,就是时区参数都是必传的。多数情况下会去获取默认时区,而调用 TimeZone.currentSystemDefault() 的代码会有点长。

最开始我是想写个扩展函数重载一下的。

fun LocalDateTime.toInstant(): Instant = toInstant(TimeZone.currentSystemDefault())
复制代码

但是仔细想了下官方明明是可以给默认参数不需要我们传,而官方却没这么做。所以个人猜测官方会不会是不建议我们频繁调用 TimeZone.currentSystemDefault() 获取默认时区,后面找到了一个 pull request 印证了我的猜想。

image.png

这是一个将 TimeZone.SYSTEM 常量改为 TimeZone.currentSystemDefault() 方法的 pr,原因是在原生环境查询时区是一项昂贵的操作,考虑到大多数 JRE 实现不会更新系统时区,所以最初定义为了一个常量只获取一次。然而发现这么做是有问题的,至少在 Android 是会变化的,假设时区不变并不可行。

那么传默认时区参数时要注意以下两点:

  • TimeZone.currentSystemDefault() 应该尽可能少地调用,那就要缓存起来。
  • Android 要考虑时区改变的情况。

自己学习的话很难发现这个细节,因为在官方文档并没有写,而是故意把获取默认时区的 API 设计得比较长,并且时区参数都是必传的。这样使用起来更繁琐,大家就可能会考虑缓存默认时区。

我们可以用到属性委托进行封装。声明一个 TimeZone 属性,在属性的委托类中声明一个 TimeZone 变量,只有在初始化和时区改变的时候才赋值 TimeZone.currentSystemDefault()。委托的代码如下:

val systemTimeZone: TimeZone by object : ReadOnlyProperty<Any?, TimeZone> {
  private lateinit var timeZone: TimeZone

  override fun getValue(thisRef: Any?, property: KProperty<*>): TimeZone {
    if (!::timeZone.isInitialized) {
      timeZone = TimeZone.currentSystemDefault()
      application.registerReceiver(object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent?) {
          if (intent?.action == Intent.ACTION_TIMEZONE_CHANGED) {
            timeZone = TimeZone.currentSystemDefault()
          }
        }
      }, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
    }
    return timeZone
  }
}
复制代码

然后可以给需要时区参数的函数重载一个扩展,传入上面的属性。

fun LocalDateTime.toInstant(): Instant = toInstant(systemTimeZone)
复制代码

这样我们不传 TimeZone 参数时,就会获取缓存的默认时区,并且这个缓存会随着时区的改变而改变。

对属性委托不熟悉的小伙伴可以看下我另一篇文章《属性委托的本质及 MMKV 的运用》

最终方案

最后介绍一下个人封装的 Kotlin 工具库 —— Longan。可能是最好用的 Kotlin 工具库,由于 Kotlin 语法糖很多,个人认为工具类的 API 设计非常重要。所以每个用法都会经过个人的大量思考,并且参考官方 KTX 库的命名规则和用法,会比多数人封装的 Kotlin 工具类更加简洁易用,功能也是用尽可能简洁轻量的代码实现。目前迭代几版后已有超过 500 个常用方法或属性,能大大提高开发效率。

最新版已经集成了 kotlinx-datetime 依赖和前面说的封装,提供了一套完整好用的日期时间功能。相较于直接使用 kotlinx-datetime,有以下改进:

  • 支持任意时间格式的转化,不限于 ISO-8601 格式。
  • 可不传时区,会用最优的方式获取默认时区。
  • 补充了更多 Java8 的功能。

开始使用

添加依赖和脱糖配置:

allprojects {
    repositories {
        // ...
        maven { url 'https://www.jitpack.io' }
    }
}
复制代码
android {
    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
    implementation 'com.github.DylanCaiCoding.Longan:longan:1.0.5'
}
复制代码

可以更简单地互转时间戳、字符串、InstantLocalDateTime,无需传时区参数。

Instant.fromEpochSeconds(1643673600).format("yyyy-MM-dd HH:mm:ss") // 时间戳转字符串
Instant.fromEpochSeconds(1643673600).toLocalDateTime()             // 时间戳转 LocalDateTime
Instant.fromEpochSeconds(1643673600)                               // 时间戳转 Instant
"2022-02-01 08:00:00".toEpochSeconds("yyyy-MM-dd HH:mm:ss")        // 字符串转时间戳
"2022-02-01 08:00:00".toLocalDateTime("yyyy-MM-dd HH:mm:ss")       // 字符串转 LocalDateTime
"2022-02-01 08:00:00".toInstant("yyyy-MM-dd HH:mm:ss")             // 字符串转 Instant
localDateTime.toInstant().epochSeconds                             // LocalDateTime 转 时间戳
localDateTime.format("2022-02-01 08:00:00")                        // LocalDateTime 转 字符串
localDateTime.toInstant()                                          // LocalDateTime 转 Instant
instant.epochSeconds                                               // Instant 转时间戳
instant.format("yyyy-MM-dd HH:mm:ss")                              // Instant 转字符串
instant.toLocalDateTime()                                          // Instant 转 LocalDateTime
复制代码

补充了更多在 Java8 有而 kotlinx-datetime 没有的功能:

val localDate = LocalDate(2022, 2, 15)
localDate.withYear(2021)               // 2021-02-15,调整年份
localDate.withMonth(5)                 // 2021-05-15,调整月份
localDate.withDayOfMonth(18)           // 2021-02-18,调整日期
localDate.withDayOfYear(60)            // 2021-03-01,调整今年的天数
localDate.firstDayOfMonth()            // 2022-02-01,这个月的第一天
localDate.lastDayOfMonth()             // 2022-02-28,这个月最后一天
localDate.firstDayOfNextMonth()        // 2022-03-01,下个月第一天
localDate.firstDayOfLastMonth()        // 2022-01-01,上个月的第一天
localDate.firstDayOfYear()             // 2022-01-01,今年的第一天
localDate.lastDayOfYear()              // 2022-12-31,今年的最后一天
localDate.firstDayOfNextYear()         // 2023-01-01,明天的第一天
localDate.firstDayOfLastYear()         // 2021-01-01,去年的第一天
localDate.firstInMonth(TUESDAY)        // 2022-02-01,这个月的第一个周二
localDate.lastInMonth(TUESDAY)         // 2022-02-22,这个月的最后一个周二
localDate.dayOfWeekInMonth(2, TUESDAY) // 2022-02-08,这个月的第二个周二
localDate.next(TUESDAY)                // 2022-02-22,下一个周二,不包含今天
localDate.previous(TUESDAY)            // 2022-02-08,上一个周二,不包含今天
localDate.nextOrSame(TUESDAY)          // 2022-02-15,下一个周二,包含今天
localDate.previousOrSame(TUESDAY)      // 2022-02-15,上一个周二,包含今天
复制代码

封装思路和前面的类似,感兴趣的可以自己看下源码

实际运用

最后用个微信时间的例子来实战一下,个人总结了微信时间的显示有以下规律:

  1. 今天的只显示时间,比如 10:08
  2. 昨天的显示昨天 + 时间,比如 昨天 18:00
  3. 早于昨天并且在本周内的显示星期 + 时间,比如 周二 06:30
  4. 不在本周但在今年的显示月份 + 时间段 + 时间,比如 2月1日 凌晨 00:01
  5. 以上都不满足则显示年月日 + 时间段 + 时间,比如 2021年10月1日 早上 10:00

上述逻辑如果用 Date 来计算肯定麻烦很多,而用 kotlinx-datetime 加上个人封装好的扩展方法,只需下面一点代码就能实现。

fun getFriendlyTimeString(millis: Long): String {
  val localDateTime = Instant.fromEpochMilliseconds(millis).toLocalDateTime()
  val today = Clock.System.today
  val pattern = when {
    localDateTime.date == today -> "HH:mm"
    localDateTime.date == today.minus(1, DAY) -> "昨天 HH:mm"
    localDateTime.date >= today.previousOrSame(MONDAY) -> "EE HH:mm"
    localDateTime.year == today.year -> "MM月dd日 ${localDateTime.timeRange} HH:mm"
    else -> "yyyy年MM月dd日 ${localDateTime.timeRange} HH:mm"
  }
  return localDateTime.format(pattern, Locale.CHINA)
}

private val LocalDateTime.timeRange: String
  get() = when (hour) {
    in 0..5 -> "凌晨"
    in 6..12 -> "早上"
    in 13..17 -> "下午"
    else -> "晚上"
  }
复制代码

是不是非常好用呢,赶紧来试试吧!!

参考文献

讲解封装思路的文章

总结

本文介绍了 Java7、Java8、kotlinx-datetime 库的时间和日期 API 的区别,介绍了 kotlinx-datetime 的类型的用法。自己看 kotlinx-datetime 官方文档学习的话,可能不知道如何转化各种时间日期格式,可能会频繁调用获取默认时区的方法,个人对此给出了一套完美的解决方案。

最后分享了个人封装的 Kotlin 工具库 Longan,基于 kotlinx-datetime 封装了一套完整好用的日期和时间功能,并且用了一个微信时间的例子实战一下。如果您觉得有帮助的话,希望能点个 star 支持一下哟 ~ 个人后面会分享更多封装相关的文章给大家。

对个人封装的库有任何使用上的问题都可以加微信 DylanCaiCoding 直接反馈,有一些封装上的问题也可以一起探讨哦~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享