HTTP is a protocol for fetching resources such as HTML documents. It is the foundation of any data exchange on the web. It is also a client-server protocol, which means requests are initiated by the recipient, usually the browser. A complete document is reconstructed from different sub-documents fetched, such as text, layout description, images, videos, scripts, and more.
Clients and servers communicate by exchanging individual messages (as opposed to a stream of data). The messages sent by the client, usually a browser, are called requests, and the answer messages sent by the server are called responses. In this topic, you will learn how to use an HTTP client library called OkHttp.
HTTP clients
To interact over HTTP, we use libraries that are called HTTP clients. Their purpose is to send HTTP requests and handle the response. These are some of the most widely used libraries:
HttpURLConnection is part of the standard Java library. It is the oldest of them all and has the most backward compatibility. It is easy to use but it only offers synchronous/blocking API and doesn't have HTTP/2 support.
Java HttpClient is another native Java library generally available in Java 11. It uses synchronous and asynchronous modes of operations and has HTTP/2 support. However, it doesn't support transparent compression.
Apache HttpClient is an external open-source library and the most commonly used client outside of the Java standard library. It has a rich set of features and configuration options. It is best to use it if you need extreme flexibility in configuring and customizing behavior.
Further in this topic, you will be learning about one of the most commonly used clients: the OkHttp client.
Why OkHttp?
OkHttp is an HTTP and HTTP/2 client used for building Java and Kotlin applications. Thanks to HTTP, you can now transfer data and media through a client-server communication model. OkHttp is efficient with:
connection pooling;
gzip support, which shrinks download size;
response caching allows the network not to repeat requests;
HTTP/2 support.
OkHttp recovers common connection problems, such as sending a request to an alternate address, if a service has multiple IP addresses. In addition, it assists with synchronous blocking calls and asynchronous calls with callbacks.
In most cases, you will communicate with Web API through OkHttp. Web APIs are helpful because they allow you to access remote data or functionalities. You would mostly want to get data from a remote database, but sometimes you have to send some data to a remote application. Now that you have learned about OkHttp and its key features, it's time to learn how to make a request with it.
GET
Thanks to OkHttp, we can easily access data using GET requests from the internet. Let's analyze the code in the snippet below:
fun getRequestExample() {
private val client = OkHttpClient()
val request = Request.Builder()
.url("https://ghibliapi.herokuapp.com/films/")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unsuccessful $response")
for ((name, value) in response.headers) {
println("$name: $value")
}
println(response.body?.string())
}
}
First, with the Request class, we set the HTTP or HTTPS address from which we want to GET data. Then, we make the call to the URL using OkHttpClient().newCall(request).execute(). After the call, we check if we have a valid HTTP request-response session with response.isSuccessful : if not, we can throw an IOException. If there is a successful connection, response.headers helps us see the response document type, server, caching info, cross-domain request policy, and so on. Let's see the response.headers output in the console:
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Server: Cowboy
X-Powered-By: Express
Vary: Origin
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
X-Content-Type-Options: nosniff
Etag: W/"8050-tpjPes9K7HqConzZaVosUd+MB3E"
Date: Wed, 31 Aug 2022 20:39:03 GMT
Via: 1.1 vegur
Finally, with response.body we access the requested data. You can see an extract of the response.body data in the snippet below:
[
{
"id": "2baf70d1-42bb-4437-b551-e5fed5a87abe",
"title": "Castle in the Sky",
"original_title": "天空の城ラピュタ",
"original_title_romanised": "Tenkū no shiro Rapyuta",
"image": "https://image.tmdb.org/t/p/w600_and_h900_bestv2/npOnzAbLh6VOIu3naU5QaEcTepo.jpg",
"movie_banner": "https://image.tmdb.org/t/p/w533_and_h300_bestv2/3cyjYtLWCBE1uvWINHFsFnE8LUK.jpg",
"description": "The orphan Sheeta inherited a mysterious crystal that links her to the mythical sky-kingdom of Laputa. With the help of resourceful Pazu and a rollicking band of sky pirates, she makes her way to the ruins of the once-great civilization. Sheeta and Pazu must outwit the evil Muska, who plans to use Laputa's science to make himself ruler of the world.",
"director": "Hayao Miyazaki",
"producer": "Isao Takahata",
"release_date": "1986",
"running_time": "124",
"rt_score": "95",
"people": [
"https://ghibliapi.herokuapp.com/people/598f7048-74ff-41e0-92ef-87dc1ad980a9",
"https://ghibliapi.herokuapp.com/people/fe93adf2-2f3a-4ec4-9f68-5422f1b87c01",
"https://ghibliapi.herokuapp.com/people/3bc0b41e-3569-4d20-ae73-2da329bf0786",
"https://ghibliapi.herokuapp.com/people/40c005ce-3725-4f15-8409-3e1b1b14b583",
"https://ghibliapi.herokuapp.com/people/5c83c12a-62d5-4e92-8672-33ac76ae1fa0",
"https://ghibliapi.herokuapp.com/people/e08880d0-6938-44f3-b179-81947e7873fc",
"https://ghibliapi.herokuapp.com/people/2a1dad70-802a-459d-8cc2-4ebd8821248b"
],
"species": [
"https://ghibliapi.herokuapp.com/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2"
],
"locations": [
"https://ghibliapi.herokuapp.com/locations/"
],
"vehicles": [
"https://ghibliapi.herokuapp.com/vehicles/4e09b023-f650-4747-9ab9-eacf14540cfb"
],
"url": "https://ghibliapi.herokuapp.com/films/2baf70d1-42bb-4437-b551-e5fed5a87abe"
},
{
"id": "12cfb892-aac0-4c5b-94af-521852e46d6a",
"title": "Grave of the Fireflies",
"original_title": "火垂るの墓",
"original_title_romanised": "Hotaru no haka",
"image": "https://image.tmdb.org/t/p/w600_and_h900_bestv2/qG3RYlIVpTYclR9TYIsy8p7m7AT.jpg",
"movie_banner": "https://image.tmdb.org/t/p/original/vkZSd0Lp8iCVBGpFH9L7LzLusjS.jpg",
"description": "In the latter part of World War II, a boy and his sister, orphaned when their mother is killed in the firebombing of Tokyo, are left to survive on their own in what remains of civilian life in Japan. The plot follows this boy and his sister as they do their best to survive in the Japanese countryside, battling hunger, prejudice, and pride in their own quiet, personal battle.",
"director": "Isao Takahata",
"producer": "Toru Hara",
"release_date": "1988",
"running_time": "89",
"rt_score": "97",
"people": [
"https://ghibliapi.herokuapp.com/people/"
],
"species": [
"https://ghibliapi.herokuapp.com/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2"
],
"locations": [
"https://ghibliapi.herokuapp.com/locations/"
],
"vehicles": [
"https://ghibliapi.herokuapp.com/vehicles/"
],
"url": "https://ghibliapi.herokuapp.com/films/12cfb892-aac0-4c5b-94af-521852e46d6a"
},POST
If you want to send something to the server, you can easily do it with a POST request. Here is how you post a String-like plain text in code:
val MEDIA_TYPE: MediaType = "text/plain; charset=utf-8".toMediaType()
fun postRequestExample() {
private val client = OkHttpClient()
val postBody = """
Learning
at
Hyperskill
is
fun
""".trimMargin()
val request = Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(postBody.toRequestBody(MEDIA_TYPE))
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unsuccessful $response")
println(response.body?.string())
}
}
To make a POST request, you must describe the type of request body. We did that with val MEDIA_TYPE. After that, we put the String (it can be also ByteArray or File) that we want to post in val postBody. Similarly to GET requests, you set the HTTP or HTTPS address on what you want to post, but with POST requests you need to add the post() method before the build() method. In the post() method, you put your String and convert it toRequestBody(contentType) . Everything else is the same as with the GET request. If you want to make a POST request with more parts, you call MultipartBody.Builder(). Then, you must call the setType() method, in which you specify the MultipartBody media type, such as FORM, ALTERNATIVE, MIXED, DIGEST and PARALLEL (we are using FORM in this example). Once it's done, you are adding the content that you want to post in .addFormDataPart() where you are putting the name of the data part and value. If you have a file that you want to post, you must put the name, filename, File() in .addFormDataPart(), and define the media type of the file. When you add the last data part, you call the build() method. Now, you can put your MulitipartBody inside the post() in your Request.Builder(). You can see an example of a multipart body in the snippet below:
private val IMGUR_CLIENT_ID = "9199fdef135c122"
private val MEDIA_TYPE_PNG = "image/png".toMediaType()
fun run() {
val client = OkHttpClient()
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Check")
.addFormDataPart(
"image", "check.png",
File("C:\\Users\\user\\Downloads\\check.png").asRequestBody(MEDIA_TYPE_PNG)
)
.build()
val request = Request.Builder()
.header("Authorization", "Client-ID $IMGUR_CLIENT_ID")
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
println(response.body!!.string())
}
}Response caching
Response caching allows OkHttp to avoid repeating requests over the network. That's amazing because it helps speed up application performance and increase efficiency. Response caches will be stored in a directory, meaning that all previously downloaded data will be accessed from locally-cached resources. In Android, a directory with cache data must be private, so untrusted applications can't access the data. The best practice is to have one instance of OkHttpClient() that is accessible throughout the whole application. Otherwise, two cache instances might stomp on each other, corrupt the response cache, and possibly crash your application. OkHttp response caching uses HTTP headers to determine cache behavior. It can be configured on the server, for example, how long responses are cached with response headers like Cache-Control: max-age=1000. You can also prevent OkHttp from using caches with CacheControl.FORCE_NETWORK. Alternatively, you can force it to use cache instead of network with CacheControl.FORCE_CACHE. All that can be done by passing a CacheControl value into cacheControl() method of Request.Builder.
Be aware that forcing the use of cache can result in response code 504 known as an Unsatisfiable request error if there is no cache data. Otherwise, it will show cached data until the latest data is downloaded.
Let's see how can you use cache in code:
fun responseCachingExample() {
private val client: OkHttpClient = OkHttpClient.Builder()
.cache(Cache(
directory = cacheDirectory, //write String path to the actual folder
maxSize = 10L * 1024L * 1024L // 10 MiB
))
.build()
val request = Request.Builder()
.url("https://ghibliapi.herokuapp.com/films/")
.build()
val response1Body = client.newCall(request).execute().use {
if (!it.isSuccessful) throw IOException("Unsuccessful $it")
println("Response 1 cache response: ${it.cacheResponse?.headers.toString()}")
return it.body?.string()
}
val response2Body = client.newCall(request).execute().use {
if (!it.isSuccessful) throw IOException("Unsuccessful $it")
println("Response 2 cache response: ${it.cacheResponse?.headers.toString()}")
return it.body?.string()
}
println("Response 2 equals Response 1? " + (response1Body == response2Body))
}
To do caching, you can't just instantiate a class by calling its OkHttpClient() . On top of that, you need to call .Builder() and the .cache() method. In cache(), you need to put a Cache() instance and in the constructor, we need to specify the cache data's maximum size and where we want to store it. Last but not least, you call the build() method to construct your client. Now, you're all set! Let's see the output.
When you start the application for the first time, your first output will be null. The second output will contain the detailed context of the response, but the response in both outputs will be the same. The last output will be that the statement "Response 2 equals Response 1" is true. How come, you might ask. The first output says that your current cache data is null. After that, the data is saved in your directory, and the second output shows us that data. The last output says only that the data that is in the response1Body && response2Body is the same.
Conclusion
In this topic, you learned about the OkHttp client. You learned about the advanced features of OkHttp and how you can use Web APIs with OkHttp. With code examples, we demonstrated how to make successful GET and POST requests. Also, you learned how to store cache data with OkHttp and make your responses load faster. Now that you finished the theory part, let's put it to the test!