Blog

Reverse Lookup Enums with Kotlin

Kotlin has a lot of super cool things, and Enum Classes are one of them.

We initialize our enums with a string value. Yet a problem arose when we wanted to find the enum using that string value. What’s the best way to find an enum when you have it’s value?

enum class TransmissionType(val dataKey: String) {
    MANUAL_TRANSMISSION("MT"),
    AUTOMATIC_TRANSMISSION("AT")
}

val myValue = "MT"
// how do I use "MT" to get MANUAL_TRANSMISSION ?

It turns out there is a idiomatic way to achieve this using associateBy and the get operator. This technique works really well.

Instead of writing a function to find your enum: MyEnumClass.findMyEnum("AT")
You have code like this: MyEnumClass["AT"]

Instead of explaining it, I’ll just show you an example with some tests.


import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

enum class TransmissionType(val dataKey: String) {
    MANUAL_TRANSMISSION("MT"),
    AUTOMATIC_TRANSMISSION("AT");

    companion object {
        private val map = TransmissionType.values().associateBy(TransmissionType::dataKey)
        operator fun get(value: String) = map[value]
    }
}

class EnumAssociateByTest {
    @Test
    fun getDataKey() {
        val equipmentType = TransmissionType.MANUAL_TRANSMISSION
        assertEquals(equipmentType.dataKey, "MT")
    }

    @Test
    fun getTypeFromDataKey() {
        // WHEN I try to get an transmission type based on its `dataKey`
        val equipment = TransmissionType["AT"]

        // THEN I get the expected result
        assertNotNull(equipment)
        assertEquals(TransmissionType.AUTOMATIC_TRANSMISSION, equipment!!)
    }

    @Test
    fun getNullFromNonExistentType() {
        // WHEN I try to get an equipment type using a non-existing `dataKey`
        val equipment = TransmissionType["nope"]
        // THEN I get the expected null value
        assertNull(equipment)
    }
}

When learning new techniques, I like to build little examples in IntelliJ with tests.
I’ve found it makes it lot faster to play around than in Android Studio.

Enjoy!

Using ConstraintLayout with Circular Positioning on Android

I was working on a feature for a client that involved custom views with Android. While researching ConstraintLayout, I saw that it had a circular positioning feature. It seemed pretty neat, so I thought I would circle around when finished and build a demo.

The basics of Circular Positioning are fairly straight forward:

  1. You define a view as the center
  2. You create a circular constraint for each view you want to surround it

I learned a lot from the following article:
https://www.journaldev.com/21366/android-constraint-layout-circular-positioning

Features

You can build a circular restraint in a layout file. It’s a great first exercise. Yet, I wanted to be able to change the number of views that circled the center. To do that I needed to create a custom view.

It seemed like it would be a good learning experience. I have never had to create constraints in code before. Judging by what I saw on the internet, few others do either.

My demo lays out my items in a circular layout and gives the option to the user to change the number of items. When changed, the view re-constrains everything in a balanced circle.

For example:
2 items = 50%, 3 items = 33.3%, 4 items = 25%, and so on

circular layout with faces
My custom view – with 8 images surrounding the center

The Custom View

My custom view is called CircularIconLayout

  • It extends ConstraintLayout – simply because I am building a ConstraintLayout!
  • I base the views in the circle on a section count
  • I pass in a layout file that is used for the views in the circle. This allows me to use any type of view I want

When putting everything together, I follow these general steps:

  1. I create a TextView and constrain it to the center (centerView)
  2. Create a separate view for each section of the circle
  3. I calculate where to place the views around the circle:
    1. divide the number of sections by 360
      • example: 360 / 4 = 25 degrees for each section
    2. determine the center of each section
      • example: 25 / 2 = 12.5 degrees is the middle of each section
  4. Create a constraint for each view using constrainCircle()

I keep a list of the views for reference (sectionList). When the user changes the number of sections, I update the list and re-constrain everything to the centerView again.

Here is some code showing how to setup the constraints:

// constrain sections to the centerView
val constraintSet = ConstraintSet()
constraintSet.clone(this)

// constrain centerView to the middle of our (parent) view
constraintSet.connect(centerView.id, ConstraintSet.TOP, this.id, ConstraintSet.TOP)
constraintSet.connect(centerView.id, ConstraintSet.BOTTOM, this.id, ConstraintSet.BOTTOM)
constraintSet.connect(centerView.id, ConstraintSet.START, this.id, ConstraintSet.START)
constraintSet.connect(centerView.id, ConstraintSet.END, this.id, ConstraintSet.END)

// calculate where to place each view surrounding the center
val distanceFromCenter = circleRadius.toDensityPixels()
val segmentDegrees: Float = 360.0f / sectionCount.toFloat()
var angle: Float = segmentDegrees / 2.0f

// constrain each section to the centerView
sectionList.forEach { section ->
    constraintSet.constrainCircle(section.id, centerView.id, distanceFromCenter, angle)
    angle += segmentDegrees
}

constraintSet.applyTo(this)

Formatting the views in the circle is left up to the Activity or Fragment that calls it. For example, the activity could use the sectionList to load images into the ImageViews that are in the circle.

Extra features

I tried to make the demo simple, but I got a little carried away in the Activity:

  • The user can change the number of items displayed in the circle
  • The user ALSO can change the type of views used in the circle. You can switch between images and text

I also added a couple other features:

icu4j or RuleBasedNumberFormat – a library for spelling out numbers. i.e. 1 = “one”, 12 = “twelve”, and so forth.

RandomColors
I found an example for creating random colors from a pool of acceptable colors on Stack Overflow. I made some improvements and used it to generate random colors.

The Demo

For me, code speaks louder than words. Checkout the demo at my github repo:
https://github.com/jjerome00/CircleLayout

demo video gif

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.