Parsing dates using java.time on Android

Having fun using java.time and Retrofit for ISO 8601 dates on Android

I was assigned a task to parse ISO 8601 dates with timezone information from an endpoint. I decided to use Java 8’s java.time library. I had to write a custom converter to use it with Retrofit because the endpoint provided the date in a different format. This is a summary of my journey.

What is java.time, and how do I use it with Android?

java.time aims to improve the date and time handling in Java. It was added to Java in JSR-310. It was included in Android as of API 26. But if you want to use it with earlier versions, you must use a library.

There are a couple of solutions, each with their own drawbacks. I settled on a library by Jake Wharton called ThreeTenABP. It has been optimized for the Android platform. Jake goes into his reasoning on the project’s main page. https://github.com/JakeWharton/ThreeTenABP

To get this to work with Retrofit, I used a library called ThreeTen-Backport-Gson-Adapter. It’s a library that provides json serialization/deserialization for java.time using Gson. https://github.com/aaronhe42/ThreeTen-Backport-Gson-Adapter

Let’s run some tests!

There are lots of ways to document your code. One of my favorites is with tests. A good test can express how to do something, and why you are doing it. It also allows a future developer to play around with the code in a safe environment.

All my examples are tests. I’ve included the classes under test with the test itself. It makes it easier to show here.

Make sure you click to expand the test, otherwise you can’t see it!

Setup environment

You can set these up in Android Studio, but you can also use IntelliJ Community Edition.

I like having IntelliJ around to run test code. Create a new Project with Gradle, and include Java and Kotlin. Then use the build.gradle file to include your dependencies just like you would in Android Studio.

My dependencies are below. Keep in mind that these will be old versions by the time you read this.

Click to see code
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    testCompile group: 'junit', name: 'junit', version: '4.12'

    /* Java 8 java.time (JSR-310) backport for Android */
    implementation 'com.jakewharton.threetenabp:threetenabp:1.2.0'
    implementation("org.aaronhe:threetenbp-gson-adapter:1.0.2") {
        exclude module: 'threetenbp'
    }
    testImplementation 'org.threeten:threetenbp:1.3.8'
}

java.time tests

I wrote some tests to see how java.time behaves. Since it’s a newer library, I figured I could encourage my team to use them to learn java.time in a playground-like environment.

Click to see java.time tests
import junit.framework.Assert.assertEquals
import org.junit.Test
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.ZoneOffset
import org.threeten.bp.ZonedDateTime
import org.threeten.bp.format.DateTimeFormatter

class JavaTimeTests {

    @Test
    fun test_basicParsing() {
        // GIVEN a date at 12:25am UTC
        val apiDate = LocalDateTime.parse("2019-01-18T00:25:00.0000000")
        val zonedDate = ZonedDateTime.of(apiDate, ZoneId.of("UTC"))

        // WHEN I convert my date to PST
        val zoneID = ZoneId.of("PST", ZoneId.SHORT_IDS)
        val inMyTimezone = zonedDate.withZoneSameInstant(zoneID)

        val isoDateTime = inMyTimezone.format(DateTimeFormatter.ISO_DATE_TIME)

        // THEN I get the expected date and time in PST (a day earlier)
        val isoDate = inMyTimezone.format(DateTimeFormatter.ISO_DATE)
        assertEquals("2019-01-17-08:00", isoDate.toString())

        // WHEN I format my date in a different pattern (using slashes)
        val patternDate = inMyTimezone.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))
        // THEN I receive a date use the expected pattern
        assertEquals("2019/01/17", patternDate.toString())

        // WHEN I format my date in a different patter (using dashes)
        val localDate = inMyTimezone.toLocalDate()
        // THEN I receive a date use the expected pattern
        assertEquals("2019-01-17", localDate.toString())
    }

    @Test
    fun test_convertDatesToUTC() {
        // GIVEN a string date and string timezone
        val usZone = ZoneId.of("America/Los_Angeles")
        val str = "1926-09-23 00:00"

        // WHEN I parse the string date to a ZonedDateTime
        val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
        val localDateAndTime = LocalDateTime.parse(str, formatter)
        val dateAndTimeInUS = ZonedDateTime.of(localDateAndTime, usZone)

        // THEN I get the expected date in PST
        // 0 AM PST
        assertEquals(0, dateAndTimeInUS.hour)

        // THEN I get the expected date in UTC
        // 8 AM UTC
        val utcDate = dateAndTimeInUS.withZoneSameInstant(ZoneOffset.UTC)

        assertEquals(8, utcDate.hour)
    }

}

Parsing dates with gson

I’ve included the smallest amount of code to see how to use gson to format the custom date.

  • I don’t show how to use retrofit here, I just use a gson builder to incorporate the converter factory
  • The Converter parses a custom shaped json object, noted in the test
Click to see code

// for EndpointFactory
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import org.aaronhe.threetengson.ThreeTenGsonAdapter

// for EndpointDate
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import org.junit.Assert.assertEquals
import org.junit.Test
import org.threeten.bp.LocalDateTime
import org.threeten.bp.Month
import org.threeten.bp.ZoneId
import org.threeten.bp.ZonedDateTime
import org.threeten.bp.format.DateTimeParseException
import java.lang.reflect.Type

/**
 * The data class for the date
 */
data class EndpointDate(
    val dateTime: ZonedDateTime,
    val timeZone: String
)

/**
 * Deserialize an EndpointDate for Gson.  This will load the proper ZonedDateTime, in the given timeZone
 */
class EndpointDateDeserializer : JsonDeserializer<EndpointDate> {
    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): EndpointDate {
        json?.let {
            val jsonObject = it.asJsonObject

            try {
                val apiDate = LocalDateTime.parse(jsonObject.get("dateTime").asString)
                val zoneString = jsonObject.get("timeZone").asString
                val zone = ZoneId.of(zoneString)

                val zonedDate = ZonedDateTime.of(apiDate, zone)
                return EndpointDate(zonedDate, zoneString)
            } catch (e: DateTimeParseException) {
                throw JsonParseException(e)
            }
        }
        throw IllegalArgumentException("unknown type: $typeOfT")
    }
}


/**
 * I just supply the gson builder
 */
object EndpointFactory {

    fun getGson(): Gson {
        val gsonBuilder = GsonBuilder()
        gsonBuilder.registerTypeAdapter(EndpointDate::class.java, EndpointDateDeserializer())
        return ThreeTenGsonAdapter.registerZonedDateTime(gsonBuilder).create()
    }
}


class DateParseTests {

    @Test
    fun test_parseEndpointDate() {

        // GIVEN a gson object with a deserializer that can parse EndpointDate objects
        val gson = EndpointFactory.getGson()

        // GIVEN json of an EndpointDate
        val json = """
            {
                "dateTime": "1564-04-23T11:25:00.0000000",
                "timeZone": "UTC"
            }
        """.trimIndent()

        // WHEN the json is parsed into a EndpointDate data class
        val endpointDate = gson.fromJson(json, EndpointDate::class.java)

        // THEN I get the expected date with proper time zone
        assertEquals(1564, endpointDate.dateTime.year)
        assertEquals(Month.APRIL, endpointDate.dateTime.month)
        assertEquals(23, endpointDate.dateTime.dayOfMonth)

        assertEquals(11, endpointDate.dateTime.hour)
        assertEquals(25, endpointDate.dateTime.minute)
        assertEquals("UTC", endpointDate.dateTime.zone.id)
    }
}

Conclusion

This was an interesting problem to solve. We had some weird timezone issues that kept popping up. Our app was going to be heavily reliant on time. I knew we needed to stop writing short-term hacks and come up with something more concrete. This process took a little more time to put in place, but it paid off in the end by clearing the road ahead of us.

A crash course in Flutter

I gave a presentation about Flutter. It’s a lightweight overview, with a short discussion afterwords.
 
I learned Flutter for a client who wanted to explore it for a project they had in mind. They wanted someone with knowledge of Android, iOS, and anything else that might come up. Learning Flutter was a lot of fun! I have been on a few mobile projects since then where Flutter might have made a lot of sense. It would be great to get the chance to use it again in the future.
 
You can view the presentation below. I should note that I had to put it together on short notice. I would have liked to have had a more organized demo, but the presentation still works.

While you are here…

While working with Flutter, I created an open-source project for validating a Drop Down in Flutter.  You can view it on my github page: https://github.com/jjerome00/dropdown_formfield_demo

Flappy Ghost

In my series of games you can’t lose, I modified a version of Flappy Bird and made it so you can’t lose.

This is an overview of the funnest parts of the project for me. If you want to see the details, review my code on github: https://github.com/jjerome00/FlappyGhost

I started with a clone of Flappy Bird called Flappy Cow, written by Lars Harmsen. I have never met Lars, but he seems to be a very talented developer.

His clone has a different theme than the original Flappy Bird. He uses trees and spiders for the obstacles, and a cow instead of a bird. You could also die. I had to change all these things.

Sprite sheet

My first order of business was to make the clone look more like the original game. This meant tracking down a lot of the artwork from the original game. I also got to play around with a sprite sheet.

The cow shown on the screen is using what’s called a sprite sheet. A sprite sheet is one big image that contains all the movements of a character. This way you only need to use one file for all your character’s movements. It’s easier to deal with.

I constructed my own sprite sheet for the bird using the same pattern in the sheet to make the bird’s wings flap. It involved lots of fun with Gimp.

FlappyGhost-bird-spritesheet
Lining up the birds. Notice the wings moving from top to bottom.

Death becomes her

There are a few ways you could die in the game:

Touching the sky
A side effect of not being able to die is that the bird can fly off the screen. You continue flying higher and higher without any repercussions.

I fixed this by modifying the collision checking and touch events. If you touched the top of the screen, that meant you were tapping too much. So I ignore the next tap. This gives the bird enough time to drop from the sky to avoid going off screen.

Touching the ground
When the game detects a collision with the ground, I add an extra tap to keep the bird from ever touching the ground.

Running into a pipe
When the bird runs into a pipe, I thought it would be funny to have the bird change into a ghost. It would glide right through the pipe as if it wasn’t there. That’s where the “Flappy Ghost” name came from.

Ghost Bird
I wanted the bird to change into a ghost when it ran into a pipe. I thought it would be funny for the bird to look like it turned into a silly representation of a ghost. I added a little bit of translucency and a silhouette of a sheet over top.

It turns out the code already provided a nice way to achieve this. There was a feature to add accomplishments to the cow in the original Flappy Cow game. At a certain score the cow would get a hat or sunglasses. There was even a score that would change the cow to a NyanCat. I used that same logic to switch the bird sprite to the ghost sprite. After the collision with the pipe wasn’t detected anymore, I switch it back to the bird.

Clean up

There were a lot of features in the clone that you would want in a real game – such as an ads, analytics and Play Services. I had to remove everything without breaking the game. It was an interesting mix of learning how the APIs were added, and then figuring out how to remove them without crashing the app.

In Conclusion

I had a lot of fun breaking this app down and building it back up. I learned a few things about writing games, and made changes by leaning on existing features.

As a consultant, I am always writing, testing, and refactoring code to make it the best it can be. It’s fun to ignore most of that and make things work as quick as possible. I think the big lesson is that making something perfect does not always fit the client’s needs.