ThreeTenABP — Android 的适配器库,为 Android 8 以下(API < 26)的设备提供 java.time API(org.threeten.bp 包)。根据 Jake Wharton (GitHub, 2023) 的规范,该库是 ThreeTen-Backport 项目的包装器,针对 Android 进行了适配,考虑了资源优化和通过 AssetManager 对 tzdata 的支持。
要点
ThreeTenABP (ThreeTen Android Backport) — Jake Wharton 创建的库,用于在旧版 Android 上使用 Java 8 日期/时间 API。它是 ThreeTen-Backport 项目的适配器,将 java.time (JSR-310) 移植到 Java 7 和 Android API < 26。
该库解决的主要问题:Android 直到版本 8 (API 26) 才在标准发行版中包含 java.time。开发人员被迫使用 java.util.Date/Calendar 或添加 Joda-Time。ThreeTenABP 提供与内置 java.time 相同的现代 API,但通过 org.threeten.bp 包。
根据 GitHub 仓库 (2023),该库针对 Android 进行了优化:tzdata 数据(IANA 时区数据库)存储在 assets 中并通过 AssetManager 加载,而不是像桌面那样通过 classpath。这减小了 APK 大小并加快了加载速度。
最新稳定版本 — 1.4.0(2021 年 8 月)。该库处于支持模式,因为随着 desugaring 的广泛普及,对其的需求正在下降,但对于最低 API < 26 的项目仍然相关。
在 Java 8 (2014) 中出现 java.time 之前,开发人员使用 java.util.Date 和 java.util.Calendar。这些类有严重的缺点:Date 是可变的,Calendar 使用不直观的常量(Calendar.JANUARY = 0),这两个类都不是线程安全的,并且在使用时区时容易出错。
Joda-Time 是 Java 8 之前的事实标准,但其创建者 Stephen Colebourne 将 java.time 设计为官方替代品,基于 Joda-Time 的经验并考虑了其缺点。java.time 包成为了 JDK 8 的一部分,但 Android 直到 API 26 才获得它。
ThreeTen-Backport — java.time 到 Java 7 的移植,由同一位作者(Stephen Colebourne)创建。它包括所有主要类:LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Instant、Duration、Period、DateTimeFormatter。ThreeTenABP 将此移植适配到 Android,添加了通过 AssetsManager 的初始化和对移动设备的优化。
因此,ThreeTenABP 允许在 Android 4.0+ (API 14+) 设备上使用现代日期/时间 API,而无需等待操作系统更新。
连接分两步完成:在 build.gradle (app-level) 中添加依赖项和在 Application 类中初始化。重要:ThreeTenABP 要求 compileSdk 不低于 21,Gradle 版本不低于 4.0。
依赖项添加到 dependencies 部分:implementation “com.jakewharton.threetenabp:threetenabp:1.4.0”。自 1.4.0 版本发布以来,该库未更新,因为它稳定且涵盖了所有必要的情况。
根据 官方文档,该库在 assets 中包含 tzdata。如果应用程序已有包含其他文件的 assets 文件夹,ThreeTenABP 可以正确共存。tzdata 压缩后大小约为 200 KB。
// build.gradle(应用级别)
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
implementation "com.jakewharton.threetenabp:threetenabp:1.4.0"
}
在使用 org.threeten.bp 中的任何类之前,需要初始化库。初始化在 Application.onCreate() 中通过调用 AndroidThreeTen.init(this) 进行一次。
初始化从 assets 加载 tzdata 数据并配置系统时钟。如果没有调用 init(),now() 方法将抛出 IllegalStateException 异常,提示库尚未初始化。
对于测试,可以使用 AndroidThreeTen.init(applicationContext, zoneId) — 显式指定时区的重载。这对于测试的可预测行为很有用。如果只需要没有 tzdata 的基本初始化 — 请使用 AndroidThreeTen.initWithoutFiles(context)。
class App : Application() {
override fun onCreate() {
super.onCreate()
AndroidThreeTen.init(this)
}
}
// 初始化后的使用
val today = LocalDate.now()
val now = LocalDateTime.now()
ThreeTenABP 提供所有主要的 java.time 类,但在 org.threeten.bp 包中。API 与原始 java.time 几乎相同,这简化了迁移到 API 26+ 的过程。
主要类:
辅助类也受支持:Clock、DayOfWeek、Month、Year、YearMonth、MonthDay。时区随库一起提供(IANA tzdata)。ThreeTenABP 1.4.0 中的 tzdata 版本对应于 2021a。
从 Android Gradle Plugin 4.0 (2020) 和 desugar_jdk_libs 开始,开发人员获得了通过 coreLibraryDesugaring 在所有 Android 版本上使用 java.time 的能力。Desugaring 转换字节码,使 java.time 调用在旧 API 上无需额外库即可工作。
Desugaring 的优点:使用原始 java.time 包(不是 org.threeten.bp),无需初始化,与 Android Studio 完全集成。缺点:需要 AGP 4.0+,增加构建时间,APK 大小可能增加 2-3 MB。
ThreeTenABP 仍然是无法将 AGP 更新到 4.0+ 的遗留项目或 APK 大小至关重要的项目的最佳选择。ThreeTenABP 设置也更简单 — 一个依赖项和一行初始化就足够了。根据 Stack Overflow (2024),大约 30% 的 minSdk < 26 的项目仍然使用 ThreeTenABP 而不是 desugaring。
第一个示例 — 通过 ThreeTenABP 处理日期。API 与 java.time 相同,但导入来自 org.threeten.bp。这允许编写迁移后只需更改导入的代码。
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalTime
import org.threeten.bp.Duration
fun isWeekend(date: LocalDate): Boolean {
val dayOfWeek = date.getDayOfWeek()
return dayOfWeek == DayOfWeek.SATURDAY ||
dayOfWeek == DayOfWeek.SUNDAY
}
fun timeBetween(
start: LocalTime, end: LocalTime
): Duration {
return Duration.between(start, end)
}
第二个示例 — 日期格式化。来自 org.threeten.bp 的 DateTimeFormatter 的工作方式与 java.time 中相同。
import org.threeten.bp.LocalDateTime
import org.threeten.bp.format.DateTimeFormatter
fun formatTimestamp(dateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
return dateTime.format(formatter)
}
第三个示例 — 使用 ZonedDateTime 和在 ThreeTenABP 中时区之间的转换。
import org.threeten.bp.ZonedDateTime
import org.threeten.bp.ZoneId
fun convertTimeZone(
time: ZonedDateTime,
targetZone: ZoneId
): ZonedDateTime {
return time.withZoneSameInstant(targetZone)
}
当 minSdk 提高到 26 时,可以放弃 ThreeTenABP 并迁移到内置 java.time。迁移过程包括几个步骤,需要仔细测试。
第一步 — 替换导入。org.threeten.bp 的导入更改为 java.time。在大多数情况下,类名匹配:LocalDate → java.time.LocalDate,ZonedDateTime → java.time.ZonedDateTime。例外是 DateTimeFormatter — 在 ThreeTenABP 中位于 org.threeten.bp.format,在 java.time 中位于 java.time.format。
第二步 — 删除初始化。不再需要 AndroidThreeTen.init(this) 行,因为 java.time 已内置到 Android SDK 中。从 Application.onCreate() 中删除调用,并从 build.gradle 中删除依赖项。
第三步 — 如果 minSdk 保持在 26 以下,将依赖项替换为 desugaring。在 compileOptions 中添加 isCoreLibraryDesugaringEnabled = true 和 desugar_jdk_libs 依赖项。这将确保在没有 ThreeTenABP 的情况下在旧 API 上运行 java.time。根据 Google I/O (2023),desugaring 是新项目的首选方法。
// build.gradle — 用 desugaring 替换 ThreeTenABP
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
// 删除:implementation “com.jakewharton.threetenabp:threetenabp:1.4.0”
// 添加:
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}
// 从 Application 中删除 AndroidThreeTen.init(this)
常见问题
从技术上讲 — 可以,但这没有意义。如果使用 desugaring,内置 java.time 已经可用。同时使用这两个库将导致代码重复和 APK 大小增加。为项目选择一种方法。
初始化将 IANA 时区数据库数据从 assets 加载到内存。在标准 JDK 上,tzdata 通过 classpath 可用,但 Android 使用 AssetManager。init() 方法将数据复制到系统目录,使其可供 ZoneId 使用。
ThreeTenABP 支持 API 14+(Android 4.0 Ice Cream Sandwich 及以上)。使用需要 Java 8 compatibility(compileOptions 中的 sourceCompatibility 和 targetCompatibility)。在 API 26+ 上不需要该库 — 请使用内置 java.time。
时区随库一起提供。版本 1.4.0 包含 tzdata 2021a。要更新,需要更新 ThreeTenABP 版本或手动替换 assets 中的 tzdata。最新版本的 tzdata 可以从 IANA 仓库或通过 ThreeTen-Backport 获取。
对于单元测试,使用带有显式时区指定的 AndroidThreeTen.init(context, zoneId)。对于 Robolectric 测试 — AndroidThreeTen.init(ApplicationProvider.getApplicationContext())。对于没有 Android 的纯 JVM 测试 — 直接使用 ThreeTen-Backport,无需 ThreeTenABP。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。