前言
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 主要包括
Date
、Calendar
和TimeZone
这几个类,Java8 主要包括LocalDateTime
、ZonedDateTime
、ZoneId
等。 - 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
复制代码
LocalDate
是 LocalDateTime
的日期组成部分。
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
表示星期和月份,可通过 LocalDateTime
、LocalDate
对象获取,或者直接使用 DayOfWeek.MONDAY
、Month.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
提供了很多处理日期和时间的类,但是我们平时开发主要是处理时间戳和具体的日期和时间,所以主要关注 Instant
、LocalDateTime
和 LocalDate
,其它的类基本是用于辅助使用的。
以下是官方给出的在什么情况下选择使用哪种类型的一些基本建议:
-
用
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.DateBased
、DatePeriod
类型,并且不需要传时区。
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 有操作符重载的语法特性,所以 Instant
、LocalDateTime
、LocalDate
能直接比大小得出时间的早晚。
if (instant1 < instant2) {
// ...
}
复制代码
if (localDateTime1 >= localDateTime2) {
// ...
}
复制代码
时间转换
在不同时区的同一时刻得到的日期和时间是不一样的,所以 Instant
和 LocalDateTime
互转会有个时区参数。
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
保存了时间戳,所以我们关心 Instant
、LocalDateTime
和字符串互转。
可是目前 Instant
、LocalDateTime
、LocalDate
仅支持 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 格式的字符串转为 Instant
、LocalDateTime
、LocalDate
。
"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 印证了我的猜想。
这是一个将 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'
}
复制代码
可以更简单地互转时间戳、字符串、Instant
、LocalDateTime
,无需传时区参数。
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,上一个周二,包含今天
复制代码
封装思路和前面的类似,感兴趣的可以自己看下源码。
实际运用
最后用个微信时间的例子来实战一下,个人总结了微信时间的显示有以下规律:
- 今天的只显示时间,比如
10:08
。 - 昨天的显示昨天 + 时间,比如
昨天 18:00
。 - 早于昨天并且在本周内的显示星期 + 时间,比如
周二 06:30
。 - 不在本周但在今年的显示月份 + 时间段 + 时间,比如
2月1日 凌晨 00:01
。 - 以上都不满足则显示年月日 + 时间段 + 时间,比如
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 -> "晚上"
}
复制代码
是不是非常好用呢,赶紧来试试吧!!
参考文献
讲解封装思路的文章
- 《如何更好地使用 Kotlin 语法糖封装工具类》
- 《属性委托的本质及 MMKV 的运用》
- 《优雅地封装和使用 ViewBinding,该替代 Kotlin synthetic 和 ButterKnife 了》
- 《 ViewBinding 巧妙的封装思路,还能这样适配 BRVAH 》
- 《优雅地处理后台返回的骚数据》
总结
本文介绍了 Java7、Java8、kotlinx-datetime
库的时间和日期 API 的区别,介绍了 kotlinx-datetime
的类型的用法。自己看 kotlinx-datetime
官方文档学习的话,可能不知道如何转化各种时间日期格式,可能会频繁调用获取默认时区的方法,个人对此给出了一套完美的解决方案。
最后分享了个人封装的 Kotlin 工具库 Longan,基于 kotlinx-datetime
封装了一套完整好用的日期和时间功能,并且用了一个微信时间的例子实战一下。如果您觉得有帮助的话,希望能点个 star 支持一下哟 ~ 个人后面会分享更多封装相关的文章给大家。
对个人封装的库有任何使用上的问题都可以加微信 DylanCaiCoding
直接反馈,有一些封装上的问题也可以一起探讨哦~