Modern DateTimes on Android

April 15, 2019

Java 8 gave us a great gift — the java.time package, known as JSR 310 and ThreeTen. The story behind java.time is unique. It was introduced in JEP 150 by an independent developer — Stephen Colebourne (@jodastephen). Yep, the same person who designed and developed Joda-Time. It was even endorsed by Brian Goetz, the Java Concurrency in Practice author! The result is a great API — explicit and direct, based on years of Joda-Time experience.

The existing Java date and time classes are poor, mutable, and have unpredictable performance. There has been a long-standing desire for a better date and time API based on the Joda-Time project. The new API will have a more intuitive design allowing code to better express its intent. The classes will also be immutable which aligns with the multi-core direction of the industry.

— JEP 150: Motivation

The message is clear — the replacement was needed. The good news — we got it. The bad news — we have…

Android

Java 8 was released in 2014, now is 2019 and we still cannot use java.time on Android without asterisks.

minSdkVersion <= 25

Use ThreeTenBP (ThreeTen backport) and ThreeTenABP (ThreeTen backport for Android).

The ThreeTenABP is not actually a full-blown ThreeTen implementation. It is a special time zones initializer which fetches data not from Java resources but from Android assets since it is more efficient.

Dependencies

Application

ThreeTenABP provides ThreeTenBP as a transitive dependency but it is useful to have the same ThreeTenBP version for…

Unit Tests

Unit tests are being run on JVM so there is no need for the Android-specific time zones initializer.

Joda-Time?

Abandon Joda-Time! Don’t hesitate to migrate from it to ThreeTenBP ASAP.

minSdkVersion >= 26

Use java.time, forget about Joda-Time and ThreeTenBP.

📖 Android uses ICU to provide time zones data.

The downside of using native java.time is updating time zones data. Since standalone distributions (such as Joda-Time and ThreeTenBP) carry their own data it is possible to update it separately. Unfortunately on Android time zones data updates depend on OEM. It is an open question which OEMs actually do this.

Usage

Access

Since ThreeTenBP without time zones data will not initialize time zones by itself, we’ll need to do it ourselves. Executing time zone-related operations without initialization will lead to runtime exceptions. It is a good idea to have a time abstraction in place which will be an entry point for time-related operations. It is a good practice to have it for testing purposes anyway.

📖 Duration is safe to use everywhere since it is basically a pair of seconds and nanoseconds with syntax sugar on top.

interface Time {

    fun now(): ZonedDateTime

    class Impl(private val context: AndroidContext) : Time {

        private val initializer: Unit by lazy {
            AndroidThreeTen.init(context)
        }

        private inline fun <T> initialized(crossinline func: () -> T): T {
            return initializer.let { func() }
        }

        override fun now() = initialized { ZonedDateTime.now() }
    }
}

This implementation provides thread-safe initialization since lazy Kotlin fields are synchronized by default.

Don’t forget that despite ThreeTenABP features efficient time zone data initializer it still takes more than 100 milliseconds to do so. To avoid blocking the main thread use background threads on the application startup to pre-initialize time zones.

Please notice that this optimization does not eliminate the Time abstraction. Since the background initialization is an async process it is possible to use ZonedDateTime before time zones were actually initialized.

class Application : android.app.Application {

    override fun onCreate() {
        super.onCreate()

        // Provide time variable via IoC container of choise.

        Schedulers.io().scheduleDirect { time.now() }
    }
}

Constants

There is a common struggle in the industry with naming variables.

companion object {
    private const val DELAY = 10
}

What does it even mean? Are we talking about seconds or hours? We can make it a bit better.

companion object {
    private const val DELAY_SECONDS = 10
}

It does not save us though. Since naming is a semantic rule, it depends on human nature and behavior. Fortunately enough we have Duration.

companion object {
    private val DELAY = Duration.ofSeconds(10)
}

Being honest — it is not a silver bullet but it reduces the confusion significantly.

MOAR

Most likely I’m forgetting a lot of things but the bulk of the time-related work comes to Duration and ZonedDateTime. The API is so good that there are no workarounds or tricky places to navigate through.

Time Out

Think about what to do with time before you run out of… time.


The title is a reference to the Charlie Chaplin movie.