Alimenta la libreria HTTP con Kotlin

1. Panoramica

In questo tutorial, daremo uno sguardo alla Fuel HTTP Library , che è, nelle parole dell'autore, la libreria di rete HTTP più semplice per Kotlin / Android. Inoltre, la libreria può essere utilizzata anche in Java.

Le caratteristiche principali della libreria includono:

  • Supporto per verbi HTTP di base (GET, POST, DELETE, ecc.) Sia richieste asincrone che di blocco
  • Possibilità di scaricare e caricare un file ( multipart / form-data )
  • Possibilità di gestire la configurazione globale
  • Moduli di serializzazione degli oggetti incorporati (Jackson, Gson, Mhosi, Forge)
  • Supporto per il modulo coroutines di Kotlin e RxJava 2.x
  • Configura facilmente il modello di progettazione del router

2. Dipendenze

La libreria è composta da diversi moduli in modo da poter includere facilmente le funzionalità di cui abbiamo bisogno. Alcuni di questi includono:

  • Un modulo per il supporto di RxJava e Coroutines di Kotlin
  • Un modulo per il supporto dei componenti Android e Android LiveData Architecture
  • Quattro moduli da cui possiamo scegliere il modulo di serializzazione degli oggetti da utilizzare: Gson, Jackson, Moshi o Forge.

In questo tutorial, ci concentreremo sul modulo principale, i moduli per Coroutines, RxJava e il modulo di serializzazione Gson:

 com.github.kittinunf.fuel fuel ${fuel.version}   com.github.kittinunf.fuel fuel-gson ${fuel.version}   com.github.kittinunf.fuel fuel-rxjava ${fuel.version}   com.github.kittinunf.fuel fuel-coroutines ${fuel.version} 

Puoi trovare le ultime versioni, su JFrog Bintray.

3. Fare richieste

Per effettuare una richiesta, Fuel fornisce un'estensione String . Inoltre e in alternativa, possiamo usare la classe Fuel che ha un metodo per ogni verbo HTTP.

Fuel supporta tutti i verbi HTTP tranne PATCH. Il motivo è che HttpClient di Fuel è un wrapper su java.net.HttpUrlConnection che non supporta PATCH.

Per risolvere il problema, HttpClient converte le richieste PATCH in una richiesta POST e aggiunge un'intestazione X-HTTP-Method-Override: PATCH , quindi dovremo assicurarci che le nostre API siano configurate per accettare questa intestazione per impostazione predefinita.

Per spiegare le funzionalità di Fuel, utilizzeremo httpbin.org, un semplice servizio di richiesta e risposta HTTP e JsonPlaceholder, una falsa API online per test e prototipi.

3.1. OTTIENI richiesta

Iniziamo a creare una semplice richiesta HTTP GET in modalità asincrona:

"//httpbin.org/get".httpGet().response { request, response, result -> //response handling }

L'utilizzo di httpGet () su una stringa ci dà una Triple .

Il risultato è una struttura dati in stile funzionale che contiene il risultato dell'operazione (riuscita o non riuscita). Rivedremo la struttura dei dati dei risultati in una fase successiva.

Possiamo anche effettuare la richiesta in modalità di blocco:

val (request, response, result) = "//httpbin.org/get" .httpGet().response()

Notare che i parametri restituiti sono gli stessi della versione asincrona, ma in questo caso il thread che ha effettuato la richiesta è bloccato.

Inoltre, c'è la possibilità di utilizzare i parametri URL codificati:

val (request, response, result) = "//jsonplaceholder.typicode.com/posts" .httpGet(listOf("userId" to "1")).response() // resolve to //jsonplaceholder.typicode.com/posts?userId=1 

Il metodo httpGet () (e altri simili) possono ricevere un elenco per codificare i parametri URL.

3.2. Richiesta POST

Possiamo effettuare richieste POST nello stesso modo di GET, utilizzando httpPost () o utilizzando il metodo post () della classe Fuel :

"//httpbin.org/post".httpPost().response{ request, response, result -> //response handling }
val (request, response, result) = Fuel.post("//httpbin.org/post") .response() 

Se abbiamo un corpo, possiamo inserirlo tramite il metodo body () in formato stringa JSON:

val bodyJson = """ { "title" : "foo", "body" : "bar", "id" : "1" } """ val (request, response, result) = Fuel.post("//jsonplaceholder.typicode.com/posts") .body(bodyJson) .response()

3.3. Altri verbi

Come per GET e POST, esiste un metodo per ciascuno dei verbi rimanenti:

Fuel.put("//httpbin.org/put") Fuel.delete("//httpbin.org/delete") Fuel.head("//httpbin.org/get") Fuel.patch("//httpbin.org/patch")

Ricorda che Fuel.patch () eseguirà una richiesta POST con un'intestazione X-HTTP-Method-Override: PATCH .

4. Configurazione

La libreria fornisce un oggetto singleton - FuelManager.instance - per gestire la configurazione globale.

Configuriamo un percorso di base, alcune intestazioni e parametri comuni. Inoltre, configuriamo alcuni intercettori.

4.1. BasePath

Usando la variabile basePath possiamo impostare un percorso comune per tutte le richieste.

FuelManager.instance.basePath = "//httpbin.org" val (request, response, result) = "/get".httpGet().response() // will perform GET //httpbin.org/get

4.2. Intestazioni

Inoltre, possiamo gestire le intestazioni HTTP comuni utilizzando la mappa baseHeaders :

FuelManager.instance.baseHeaders = mapOf("OS" to "Debian")

In alternativa, se vogliamo impostare un'intestazione locale, possiamo utilizzare il metodo header () sulla richiesta:

val (request, response, result) = "/get" .httpGet() .header(mapOf("OS" to "Debian")) .response()

4.3. Params

Infine, possiamo anche impostare parametri comuni utilizzando l' elenco baseParams :

FuelManager.instance.baseParams = listOf("foo" to "bar")

4.4. Altre opzioni

Ci sono molte altre opzioni che possiamo gestire tramite FuelManager:

  • keystore che è nullo per impostazione predefinita
  • socketFactory che verrà fornito dall'utente o derivato dal keystore se non è nullo
  • 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) -> Unit) fun responseString(handler: (Request, Response, Result) -> Unit) fun responseJson(handler: (Request, Response, Result) -> Unit) fun  responseObject(deserializer: ResponseDeserializable, handler: (Request, Response, Result) -> Unit) 

Let's get a response as a String to illustrate this:

val (request, response, result) = Fuel.post("//httpbin.org/post") .responseString() val (payload, error) = result // payload is a String

Note that the response in JSON format requires Android dependencies.

 com.github.kittinunf.fuel fuel-android ${fuel.version} 

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
    
      { override fun deserialize(content: String): Array = Gson().fromJson(content, Array::class.java) } }
    

We can deserialize objects with custom deserializer:

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

Or via responseObject which uses internal Gson deserializer:

"//jsonplaceholder.typicode.com/posts/1" .httpGet().responseObject { _, _, 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("//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("//httpbin.org/bytes/32768") .destination { response, url -> File.createTempFile("temp", ".tmp") }

We can also download a file with a progress handler:

Fuel.download("//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
    
     > fun Request.rx_responseString(charset: Charset): Single
     
      > fun Request.rx_responseObject(deserializable: Deserializable): Single
      
       > fun Request.rx_data(): Single
       
         fun Request.rx_string(charset: Charset): Single
        
          fun Request.rx_object(deserializable: Deserializable): Single
         
        
       
      
     
    

Note that, to support all different response types, each method returns a different Single .

We can easily use “Rx” methods by invoking the more relevant one over a Request:

 "//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("//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("//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 = "//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
    
     ? get() { return when(this) { is PostRoutingAPI.posts -> listOf("userId" to this.userId) is PostRoutingAPI.comments -> listOf("postId" to this.postId) } } override val headers: Map? 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.

Quindi, lo usiamo nello stesso modo in cui abbiamo fatto tutto il tutorial con il metodo request () :

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

In questo articolo, abbiamo mostrato la libreria HTTP di carburante per Kotlin e le sue funzionalità più utili per qualsiasi caso d'uso.

La libreria è in continua evoluzione, quindi dai un'occhiata al loro repository GitHub - per tenere traccia delle nuove funzionalità.

Come al solito, tutti i frammenti di codice menzionati nel tutorial possono essere trovati nel nostro repository GitHub.