1. Overview
In this tutorial, we’ll have a look at the Fuel HTTP Library, which is, in the author’s words, the easiest HTTP networking library for Kotlin/Android. Furthermore, the library can also be used in Java.
The main features of the library include:
- Support for basic HTTP verbs (GET, POST, DELETE, etc.) and both asynchronous and blocking requests
- Ability to download and upload a file (multipart/form-data)
- Possibility to manage global configuration
- Built-in object serialization modules (Jackson, Gson, Mhosi, Forge)
- Support for Kotlin’s coroutines module and RxJava 2.x
- Easily set up the Router design pattern
2. Dependencies
The library is composed of different modules, so we can easily include the features we need. Some of these include:
- A module for RxJava and Kotlin’s Coroutines support
- A module for Android and Android LiveData Architecture Components support
- Four modules from which we can choose the object serialization module to use – Gson, Jackson, Moshi, or Forge.
In this tutorial, we’ll focus on the core module, the modules for Coroutines, RxJava, and the Gson serialization module:
<dependency>
<groupId>com.github.kittinunf.fuel</groupId>
<artifactId>fuel</artifactId>
<version>${fuel.version}</version>
</dependency>
<dependency>
<groupId>com.github.kittinunf.fuel</groupId>
<artifactId>fuel-gson</artifactId>
<version>${fuel.version}</version>
</dependency>
<dependency>
<groupId>com.github.kittinunf.fuel</groupId>
<artifactId>fuel-rxjava</artifactId>
<version>${fuel.version}</version>
</dependency>
<dependency>
<groupId>com.github.kittinunf.fuel</groupId>
<artifactId>fuel-coroutines</artifactId>
<version>${fuel.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
We can find the latest versions over on Maven Central Repository.
3. Making Requests
To make a request, Fuel provides a String extension. In addition, and as an alternative, we can use the Fuel class that has a method for each HTTP verb.
Fuel supports all HTTP verbs except for PATCH. The reason is that Fuel’s HttpClient is a wrapper over java.net.HttpUrlConnection which doesn’t support PATCH.
To side-step the problem, the HttpClient converts PATCH requests to a POST request and adds an X-HTTP-Method-Override: PATCH header, so we’ll need to make sure our APIs are configured to accept this header by default.
In order to explain Fuel’s features, we’re going to use httpbin.org, a simple HTTP request and response service, and JsonPlaceholder – a fake online API for testing and prototyping.
3.1. GET Request
Let’s start creating a simple HTTP GET request in async mode:
"http://httpbin.org/get".httpGet().response {
request, response, result ->
//response handling
}
Using httpGet() over a String is giving us a Triple<Request, Response, Result>.
The Result is a functional-style data structure that contains the result of the operation (success or failure). We’ll revisit Result data structure at a later stage.
We can also make the request in blocking mode:
val (request, response, result) = "http://httpbin.org/get"
.httpGet().response()
Note that the returned parameters are the same as the async version, but in this case, the thread which did the request is blocked.
Also, there’s a possibility to use encoded URL params:
val (request, response, result) =
"https://jsonplaceholder.typicode.com/posts"
.httpGet(listOf("userId" to "1")).response()
// resolve to https://jsonplaceholder.typicode.com/posts?userId=1
The httpGet() method (and the other similar ones) can receive a List
3.2. POST Request
We can make POST requests in the same way as for GET, using httpPost() or using the post() method of the Fuel class:
"http://httpbin.org/post".httpPost().response{
request, response, result ->
//response handling
}
val (request, response, result) = Fuel.post("http://httpbin.org/post")
.response()
If we have a body, we can put it through the body() method in JSON string format:
val bodyJson = """
{ "title" : "foo",
"body" : "bar",
"id" : "1"
}
"""
val (request, response, result) = Fuel.post("https://jsonplaceholder.typicode.com/posts")
.body(bodyJson)
.response()
3.3. Other Verbs
Same as for GET and POST, there is a method for each of the remaining verbs:
Fuel.put("http://httpbin.org/put")
Fuel.delete("http://httpbin.org/delete")
Fuel.head("http://httpbin.org/get")
Fuel.patch("http://httpbin.org/patch")
Remember that Fuel.patch() will perform a POST request with aX-HTTP-Method-Override: PATCH header.
4. Configuration
The library supplies a singleton object – FuelManager.instance – to manage global configuration.
Let’s configure a base path, some headers, and common parameters. Also, let’s configure some interceptors.
4.1. BasePath
Using the basePath variable, we can set a common path for all requests.
FuelManager.instance.basePath = "http://httpbin.org"
val (request, response, result) = "/get".httpGet().response()
// will perform GET http://httpbin.org/get
4.2. Headers
Furthermore, we can manage common HTTP headers using the baseHeaders map:
FuelManager.instance.baseHeaders = mapOf("OS" to "Debian")
In an alternative way, if we want to set a local header, we can use the header() method on the request:
val (request, response, result) = "/get"
.httpGet()
.header(mapOf("OS" to "Debian"))
.response()
4.3. Params
Finally, we can also set common parameters using the baseParams list:
FuelManager.instance.baseParams = listOf("foo" to "bar")
4.4. Other Options
There are many more options that we can manage through FuelManager:
- keystore, which is null by default
- socketFactory that will be provided by the user or derived from keystore if it is not null
- hostnameVerifier that is set by default to use the one provided by HttpsURLConnection class
- requestInterceptors and responseInterceptors
- timeout and timeoutRead for a request
4.5. Request/Response Interceptors
Regarding interceptors, we can add supplied request/response interceptors like LogRequestAsCurlInterceptor, or we can define ours:
FuelManager.instance.addRequestInterceptor(LogRequestAsCurlInterceptor)
FuelManager.instance.addRequestInterceptor(tokenInterceptor())
fun tokenInterceptor() = {
next: (Request) -> Request ->
{ req: Request ->
req.header(mapOf("Authorization" to "Bearer AbCdEf123456"))
next(req)
}
}
5. Response Handling
Previously, we introduced a functional data structure – Result – that represents the operation result (success or failure).
Working with Result is easy. It is a data class that can contain the response in ByteArray, String, JSON, or a generic T object:
fun response(handler: (Request, Response, Result<ByteArray, FuelError>) -> Unit)
fun responseString(handler: (Request, Response, Result<String, FuelError>) -> Unit)
fun responseJson(handler: (Request, Response, Result<Json, FuelError>) -> Unit)
fun <T> responseObject(deserializer: ResponseDeserializable<T>,
handler: (Request, Response, Result<T, FuelError>) -> Unit)
Let’s get a response as a String to illustrate this:
val (request, response, result) = Fuel.post("http://httpbin.org/post")
.responseString()
val (payload, error) = result // payload is a String
Note that the response in JSON format requires Android dependencies.
<dependency>
<groupId>com.github.kittinunf.fuel</groupId>
<artifactId>fuel-android</artifactId>
<version>${fuel.version}</version>
</dependency>
6. JSON Serialization/Deserialization
Fuel provides built-in support for response deserialization with four methods which, depending on our needs and on the JSON parsing library we choose, we’re required to implement:
public fun deserialize(bytes: ByteArray): T?
public fun deserialize(inputStream: InputStream): T?
public fun deserialize(reader: Reader): T?
public fun deserialize(content: String): T?
By including the Gson module, we can deserialize and serialize objects:
data class Post(var userId:Int, var id:Int, var title:String, var body:String) {
class Deserializer : ResponseDeserializable<Array<Post>> {
override fun deserialize(content: String): Array<Post>
= Gson().fromJson(content, Array<Post>::class.java)
}
}
We can deserialize objects with a custom deserializer:
"https://jsonplaceholder.typicode.com/posts"
.httpGet().responseObject(Post.Deserializer()){
_,_, result ->
val postsArray = result.component1()
}
Or via responseObject
"https://jsonplaceholder.typicode.com/posts/1"
.httpGet().responseObject<Post> { _, _, result ->
val post = result.component1()
}
On the other hand, we can serialize using Gson().toJson():
val post = Post(1, 1, "Lorem", "Lorem Ipse dolor sit amet")
val (request, response, result)
= Fuel.post("https://jsonplaceholder.typicode.com/posts")
.header("Content-Type" to "application/json")
.body(Gson().toJson(post).toString())
It’s important to set the Content-Type. Otherwise, the server may receive the object within another JSON object.
Eventually, in a similar way, we can do it by using Jackson, Moshi or Forge dependencies.
7. Download and Upload File
The Fuel library includes all the necessary features to download and upload files.
7.1. Download
With the download() method, we can easily download a file and save it into the file returned by the fileDestination() lambda*:*
Fuel.download("http://httpbin.org/bytes/32768")
.fileDestination { response, request ->
File.createTempFile("temp", ".tmp")
}
We can also download a file with a progress handler:
Fuel.download("http://httpbin.org/bytes/327680")
.progress { readBytes, totalBytes ->
val progress = readBytes.toFloat() / totalBytes.toFloat()
//...
}
7.2. Upload
In the same way, we can upload a file using upload() method, indicating the file to upload with the source() method:
Fuel.upload("/upload").source { request, url ->
File.createTempFile("temp", ".tmp")
}
Note that upload() uses the POST verb by default. If we want to use another HTTP verb, we can specify it:
Fuel.upload("/upload", Method.PUT).source { request, url ->
File.createTempFile("temp", ".tmp")
}
Moreover, we can upload multiple files using the sources() method, which accepts a list of files:
Fuel.upload("/post").sources { request, url ->
listOf(
File.createTempFile("temp1", ".tmp"),
File.createTempFile("temp2", ".tmp")
)
}
Lastly, we can upload a blob of data from an InputStream:
Fuel.upload("/post").blob { request, url ->
Blob("filename.png", someObject.length, { someObject.getInputStream() })
}
8. RxJava and Coroutines Support
Fuel provides support for RxJava and Coroutines, two way of writing asynchronous, non-blocking code.
RxJava is a Java VM implementation of Reactive Extensions, a library for composing asynchronous and event-based programs.
It extends the Observer pattern to support sequences of data/events and adds operators that allow composing sequences together declaratively without worrying about synchronization, thread safety, and concurrent data structures.
Kotlin’s Coroutines are like light-weight threads, and, as such, they can run in parallel, wait for each other and communicate… The biggest difference is that coroutines are very cheap; we can create thousands of them and pay very little in terms of memory.
8.1. RxJava
To support RxJava 2.x, Fuel provides a set of extensions:
fun Request.rxResponse(): Single<Pair<Response, Result<ByteArray, FuelError>>>
fun Request.rxResponseString(charset: Charset): Single<Pair<Response, Result<String, FuelError>>>
fun <T : Any> Request.rxResponseObject(deserializable: Deserializable<T>): Single<Pair<Response, Result<T, FuelError>>>
fun Request.rxString(charset: Charset): Single<Result<String, FuelError>>
fun <T : Any> Request.rxObject(deserializable: Deserializable<T>): Single<Result<T, FuelError>>
...
Note that, to support all different response types, each method returns a different Single<Result<>>.
We can easily use “Rx” methods by invoking the more relevant one over a Request:
"https://jsonplaceholder.typicode.com/posts?id=1"
.httpGet().rxObject(Post.Deserializer()).subscribe{
res, throwable ->
val post = res.component1()
}
8.2. Coroutines
With the coroutines module, Fuel provides extension functions to wrap a response inside a coroutine and handle its result.
To use Coroutines, similar APIs are made available, e.g responseString() became awaitStringResponse():
runBlocking {
Fuel.get("http://httpbin.org/get").awaitStringResponse()
}
It also provides useful methods for handling objects other than String or ByteArray (awaitByteArrayResponse()) using awaitObject(), awaitObjectResult() or awaitObjectResponse():
runBlocking {
Fuel.get("https://jsonplaceholder.typicode.com/posts?id=1")
.awaitObjectResult(Post.Deserializer())
}
9. API Routing
Last but not least, in order to handle network routes, Fuel provides support by implementing the Router design pattern.
With the router pattern, we can centralize the management of the API using the FuelRouting interface, which provides a combination of methods for setting the appropriate HTTP verb, path, params, and headers according to the endpoint called.
The interface defines five properties by which it is possible to configure our Router:
sealed class PostRoutingAPI : FuelRouting {
override val basePath = "https://jsonplaceholder.typicode.com"
class Post(val id: String, override val body: String?) : PostRoutingAPI()
class Comment(val postId: String, override val body: String?) : PostRoutingAPI()
override val method: Method
get() {
return when (this) {
is Post -> Method.GET
is Comment -> Method.GET
}
}
override val path: String
get() {
return when (this) {
is Post -> "/posts"
is Comment -> "/comments"
}
}
override val params: List<Pair<String, Any?>>?
get() {
return when (this) {
is Post -> listOf("id" to this.id)
is Comment -> listOf("postId" to this.postId)
}
}
override val headers: Map<String, HeaderValues>?
get() = null
override val bytes: ByteArray?
get() = null
}
In order to choose which HTTP verb to use, we have method property. Likewise, we can override the path property so as to choose the appropriate path.
Even more with the params property, we have the opportunity to set the parameters of the request, and if we need to set HTTP headers, we can do it overriding the concerning property.
Hence, we use it in the same way we had all over the tutorial with the request() method:
Fuel.request(PostRoutingAPI.Post("1",null))
.responseObject(Post.Deserializer()) {
request, response, result ->
//response handling
}
Fuel.request(PostRoutingAPI.Comment("1",null))
.responseString { request, response, result ->
//response handling
}
10. Conclusion
In this article, we’ve shown the Fuel HTTP Library for Kotlin and its more useful features for any use case.
The library is constantly evolving. Therefore, have a look at their GitHub repo – to keep track of new features.
As usual, all of the code snippets mentioned in the tutorial can be found in our GitHub repository.