Fuel HTTP Library with Kotlin

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.) 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 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>

You can find the latest versions, over on JFrog Bintray.

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 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<Pair> to encode URL parameters.

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 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 params. Also, let’s configure some interceptors.

4.1. BasePath

Using 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 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 cUrlLoggingRequestInterceptors(), or we can define ours:

FuelManager.instance.addRequestInterceptor(cUrlLoggingRequestInterceptor())
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 custom deserializer:

"https://jsonplaceholder.typicode.com/posts"
  .httpGet().responseObject(Post.Deserializer()){
    _,_, result ->
      val postsArray = result.component1()
  }

Or via responseObject<T> which uses internal Gson deserializer:

"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 destination() lambda:

Fuel.download("http://httpbin.org/bytes/32768")
  .destination { response, url ->
    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 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 asyncrhonus, 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 six extensions:

fun Request.rx_response(): Single<Pair<Response, Result<ByteArray, FuelError>>>
fun Request.rx_responseString(charset: Charset): Single<Pair<Response, Result<String, FuelError>>>
fun <T : Any> Request.rx_responseObject(deserializable: Deserializable<T>):
  Single<Pair<Response, Result<T, FuelError>>>
fun Request.rx_data(): Single<Result<ByteArray, FuelError>>
fun Request.rx_string(charset: Charset): Single<Result<String, FuelError>>
fun <T : Any> Request.rx_object(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().rx_object(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())
}

Remember that Kotlin’s Coroutines are experimental, which means that it might be changed in the upcoming releases.

9. API Routing

Last but not least, in order to handle network routes, Fuel provides the 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 {
    class posts(val userId: String, override val body: String?): PostRoutingAPI()
    class comments(val postId: String, override val body: String?): PostRoutingAPI()
    override val basePath = "https://jsonplaceholder.typicode.com"

    override val method: Method
        get() {
            return when(this) {
                is PostRoutingAPI.posts -> Method.GET
                is PostRoutingAPI.comments -> Method.GET
            }
        }

    override val path: String
        get() {
            return when(this) {
                is PostRoutingAPI.posts -> "/posts"
                is PostRoutingAPI.comments -> "/comments"
            }
        }

    override val params: List<Pair<String, Any?>>?
        get() {
            return when(this) {
                is PostRoutingAPI.posts -> listOf("userId" to this.userId)
                is PostRoutingAPI.comments -> listOf("postId" to this.postId)
            }
        }

    override val headers: Map<String, String>?
        get() {
            return 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.posts("1",null))
  .responseObject(Post.Deserializer()) {
      request, response, result ->
        //response handling
  }
Fuel.request(PostRoutingAPI.comments("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.

Leave a Reply

Your email address will not be published.