Java 8+ APIs via Desugaring
If you're still using Joda-Time or ThreeTenABP in 2026, desugaring means you can stop. Core library desugaring backports java.time, streams, and other Java 8+ APIs to API 21+ devices, letting you write standard Java/Kotlin code without platform version checks. I've migrated four codebases off Joda-Time to java.time via desugaring; the API is cleaner, the timezone handling is better, and the migration was mostly mechanical. The setup is three lines in Gradle — the gotchas are knowing what's supported and not mixing desugared APIs with legacy date libraries.
Enable desugaring
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.2")
}
That's it. Use java.time anywhere in your codebase:
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
val now = Instant.now()
val date = LocalDate.now(ZoneId.of("America/Chicago"))
val formatted = DateTimeFormatter.ISO_LOCAL_DATE.format(date)
Works on API 21+. No @RequiresApi checks needed.
What you can use
Commonly desugared APIs:
| Package | Examples | Notes |
|---|---|---|
java.time |
Instant, LocalDate, ZonedDateTime, Duration, Period | Full JSR-310 |
java.util.stream |
Stream, Collectors, IntStream | Collection operations |
java.util.function |
Function, Predicate, Consumer | Functional interfaces |
java.util |
Optional, Map.entry() | |
java.nio |
ByteBuffer (partial) | Not full NIO |
Check the desugar_jdk_libs release notes for the latest additions — Google adds APIs regularly.
What you cannot desugar
java.nio.file— no filesystem NIO on older Androidjava.lang.invoke— method handlesjava.util.concurrentadditions beyond basic support- Most Java 11+ APIs (var handles, etc.)
For unsupported APIs, use Android-specific alternatives or kotlinx libraries.
kotlinx-datetime as an alternative
For Kotlin Multiplatform projects, kotlinx-datetime works across platforms without desugaring:
// KMP: use kotlinx-datetime
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
// Android-only: java.time via desugaring is fine
import java.time.Instant
In KMP shared code, use kotlinx-datetime. In Android-only modules, java.time via desugaring is simpler and has better ecosystem support (Room TypeConverters, API serializers).
Room and java.time
Room supports java.time with TypeConverters:
class Converters {
@TypeConverter
fun fromInstant(value: Instant?): Long? = value?.toEpochMilli()
@TypeConverter
fun toInstant(value: Long?): Instant? = value?.let { Instant.ofEpochMilli(it) }
}
Store as epoch millis or ISO strings in SQLite. Avoid storing formatted date strings — they're impossible to query.
Migration from Joda-Time / ThreeTenABP
Mechanical replacements:
| Joda-Time | java.time |
|---|---|
DateTime.now() |
Instant.now() or ZonedDateTime.now() |
LocalDate.parse(str) |
LocalDate.parse(str) (same!) |
date.toDate() |
Date.from(instant) |
Interval |
Duration.between(a, b) |
DateTimeZone.forID("UTC") |
ZoneId.of("UTC") |
Remove Joda-Time/ThreeTenABP dependencies after migration:
// Remove these
// implementation("joda-time:joda-time:2.x")
// implementation("com.jakewharton.threetenabp:threetenabp:1.x")
Search for remaining imports:
grep -r "org.joda" src/
grep -r "org.threeten" src/
D8/R8 interaction
Desugaring runs before R8 shrinking. R8 removes unused desugared classes, keeping APK impact minimal. If you only use Instant and LocalDate, you won't bundle the entire java.time package.
Ensure you're on AGP 8.0+ and R8 full mode for best shrinking:
android {
buildTypes {
release {
isMinifyEnabled = true
}
}
}
API 26+ native vs desugared
On API 26+ devices, Android provides native java.time. Desugaring uses the native implementation when available and falls back to the backport on older devices. No performance penalty on modern devices.
If your minSdk is 26+, you don't need desugaring for java.time — but enabling it anyway doesn't hurt and keeps your code portable if minSdk drops.
kotlinx-datetime vs java.time on Android
For Kotlin Multiplatform projects, prefer kotlinx-datetime in shared code:
// commonMain
import kotlinx.datetime.Instant
// androidMain — convert at boundary
fun Instant.toJavaInstant(): java.time.Instant =
java.time.Instant.ofEpochSecond(epochSeconds, nanosecondsOfSecond.toLong())
Use java.time directly in Android-only modules when not sharing with iOS. Mixing both in the same layer creates conversion boilerplate — pick one per module boundary.
Common desugaring pitfalls
| Issue | Symptom | Fix |
|---|---|---|
| Missing desugar dependency | NoClassDefFoundError: java/time/Instant on API 24 |
Add desugar_jdk_libs |
| Wrong AGP version | Desugaring silently skipped | AGP 7.0+ required |
| Instant in Parcelable | Crash on process death | Store epoch millis Long |
| ZoneId everywhere | APK bloat from tz data | Use UTC internally, convert at UI |
Store timestamps as Long (epoch millis UTC) in Room — avoids serializer complexity and timezone bugs in SQL queries.
Testing across API levels
Run instrumented tests on API 24 emulator specifically for date code paths — your API 34 dev device hides desugaring bugs. CI matrix: {24, 26, 34} covers backport, native transition, and current.
Pair with Android DataStore migration when migrating date fields from SharedPreferences strings to typed storage.
Common production mistakes
Teams get desugaring java time wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
Shipping desugaring java time on Android fails quietly when you test only on flagship devices, skip process-death scenarios, or assume minSdk behavior matches latest API docs. Emulator-only validation misses OEM-specific battery optimizations and background execution limits.
Resources
- Java 8+ API desugaring support (Android)
- desugar_jdk_libs changelog
- Java time API tutorial (Oracle)
- Room TypeConverters reference
- kotlinx-datetime for multiplatform
Frequently asked questions
What is core library desugaring on Android?
Core library desugaring rewrites Java 8+ platform APIs (java.time, streams, Optional, etc.) to work on Android versions below API 26, where these APIs aren't natively available. The desugaring library provides backported implementations that are bundled into your APK.
Which Java APIs can be desugared on Android?
java.time (Instant, LocalDate, ZonedDateTime, Duration, etc.), java.util.stream, java.util.function, Optional, and newer collections APIs. Not everything is supported — java.nio.file and some concurrent APIs are unavailable. Check the desugaring library release notes for the current supported set.
Does desugaring increase APK size?
Yes, modestly — typically 100–300KB for java.time alone. The desugaring library includes only the classes your app actually references (via R8 shrinking). For most apps the size cost is far outweighed by the developer productivity gain of using java.time instead of ThreeTenABP or Joda-Time.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →