ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Kotlin: do you need (another) HTTP client?

Alexey Soshin
ProAndroidDev
Published in
4 min readAug 31, 2018

--

“clouds enveloping brown rockhill during daytime” by Sam S on Unsplash

It dawned on me while I was attending a local hackaton. Attendees were requested to get some results from a public API. And suddenly, people around me were arguing which HTTP client to use. Some went for RestTemplate, others for Apache HTTP Client, yet others tried something like JSoup.

It turns out most of the seasoned Java developers weren’t aware that Java standard library already has an HTTP client built it. It’s just called URL

To improve that situation, let’s take a simple task, like parsing response from GitHub API, without an external HTTP client (we’ll still use some library to parse JSON, though).

We’ll start with the data class, as it will help us figure out what we want to get:

data class Repo(val name: String,
val url: String,
val topics: List<String>,
val updatedAt: LocalDateTime)

Since we’ll be using Kotlin, it’s only natural that the URL we’ll be getting will belong to JetBrains:

https://api.github.com/orgs/jetbrains/repos

Our initial code will look something like this:

Since GitHub API is paginated, we fetch our repos page by page, until there are no more pages left.

Of course there are other nicer ways to write the same logic, but that’s not the focus of this article.

Now we’ll add some code to print those results, in descending order, sorted by updatedAt field:

allRepos.sortedByDescending { 
it
.updatedAt
}.forEach {
println(it)
}

Now we’re all set to do some networking.

To get contents from a remote URL we can simply use:

URL(url).openStream().use { 
it // InputStream
}

Calling use will automatically close the stream when we exit the block.

Common mistake related to use on streams is to try to return a reader from use block like that:

Everything you need to do before the stream is closed must be done inside the use block:

Now, suppose we have this input stream representing JSON, how can we parse it?

One option is to use Jackson:

compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'

The common option is to use ObjectMapper().readValue() method.

But since the response of this specific API is JSON array, parsing it becomes a bit cumbersome:

val repos: List<Repo> = ObjectMapper().readValue(it,
(object : TypeReference<List<Repo>>() {}) )

Short, but ugly.

And turns out it doesn’t work:

InvalidDefinitionException: Cannot construct instance of `Repo` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

One option to fix that is to add another dependency:

compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"

And use the new jacksonObjectMapper() function:

val repos: List<Repo> = jacksonObjectMapper().readValue(it)

But that will create more problems that it will resolve.

What if there was another way to parse JSON, which would give us slightly more control?

Luckily, it exists in the form of readTree()

val result = ObjectMapper().readTree(it).map { node -> // JsonNode
// Our parsing code comes here

}

Now we can iterate over each object in the JSON array returned by the API, and start parsing them.

Getting some properties is very trivial:

But since topics are nested JSON array, which is also optional, we use safe call and map the values:

Finally, we return our newly created data class:

Repo(name,
htmlUrl,
topics=topics,
updatedAt=LocalDateTime.parse(updatedAt))

Does it work? Of course it doesn’t! We get the following exception:

DateTimeParseException: Text '2018-08-09T04:43:06Z' could not be parsed, unparsed text found at index 19

Seems that GitHub is using different format than what LocalDateTime uses by default (which is ISO_LOCAL_DATE_TIME).

Let’s fix that by using the correct date-time format for GitHub:

updatedAt=LocalDateTime.parse(updatedAt, DateTimeFormatter.ISO_OFFSET_DATE_TIME)

That’s much better. But there’s another problem. Our topics always return empty:

Repo(name=Chocolatey, url=https://github.com/JetBrains/Chocolatey, topics=[], updatedAt=2017-03-29T02:41:51)

After a quick skim through GitHub API documentation you may notice that to get the topics, you’re required to set a specific header:

Accept: application/vnd.github.mercy-preview+json

But how do you do that with URL? Do you need to forget about it and go back to the conventional HTTP clients?

Of course I didn’t bring you all the way here just to tell you that “sorry, but our princess is in another castle”. We’ll just have to use another, lower level API for that, called openConnection()

One doesn’t simply use openConnection(), of course:

URL(url).openConnection().use { ... } // Won't work

You’ll have to get the input stream from it:

URL(url).openConnection().
getInputStream().use { ... }

That’s what openStream() method did for us before.

Now having that connection, we can set headers on it:

openConnection().setRequestProperty("Accept", "application/vnd.github.mercy-preview+json")

But the problem is that setRequestProperty() is not fluent. You cannot nicely chain it and call it like that:

Luckily, in Kotlin we have apply() to solve that:

And our topics parsed as expected now:

Repo(name=Chocolatey, url=https://github.com/JetBrains/Chocolatey, topics=[choco, chocolatey, chocolatey-packages, jetbrains], updatedAt=2017–03–29T02:41:51)

Final code looks like this:

This could be shortened

If you won’t want to concert yourself with streams and headers, and also (very) confident that your responses would fit into memory, there’s also a simpler way provided by Kotlin: readText() method:

Summary

So, this article demonstrates that it’s not mandatory to introduce another dependency for simple HTTP use cases in either Kotlin or Java.

Does that mean you should stop using HTTP clients you’re using now?

Probably not. HTTP client libraries have a lot of great features, like following redirects, caching responses, handling security, and much more.

But make sure that you consider carefully the use cases you have. And don’t bring the gorilla, if you only need the banana.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Alexey Soshin

Solutions Architect @Depop, author of “Kotlin Design Patterns and Best Practices” book and “Pragmatic System Design” course

Responses (1)

Write a response