Ktor en tant que client HTTP pour Android

J'aime Retrofit2 en tant que développeur Android, mais qu'en est-il d'essayer d'obtenir la qualité du client HTTP Ktor? À mon avis, pour le développement Android, ce n'est ni pire ni meilleur, juste l'une des options, bien que si vous envelopper un peu tout, cela puisse très bien se passer. J'examinerai les fonctionnalités de base avec lesquelles il sera possible de commencer à utiliser Ktor en tant que client HTTP - création de différents types de demandes, réception de réponses brutes et de réponses sous forme de texte, désérialisation de json en classes via des convertisseurs et journalisation.



En général, Ktor est un framework qui peut agir comme un client HTTP. Je vais le considérer du côté du développement pour Android. Il est peu probable que vous voyiez ci-dessous des cas d'utilisation très complexes, mais les fonctionnalités de base sont précises. Le code des exemples ci-dessous peut être consulté sur GitHub .

Ktor utilise les coroutines de Kotlin 1.3, une liste des artefacts disponibles peut être trouvée ici , la version actuelle est 1.0.1 .
Pour les requêtes, j'utiliserai HttpBin .

Utilisation simple


Pour commencer, vous aurez besoin de dépendances de base pour le client Android:

 implementation "io.ktor:ktor-client-core:1.0.1" implementation "io.ktor:ktor-client-android:1.0.1" 

N'oubliez pas d'ajouter au manifeste que vous utilisez Internet.

 <uses-permission android:name="android.permission.INTERNET"/> 

Essayons d'obtenir la réponse du serveur sous forme de chaîne, quoi de plus simple?

 private const val BASE_URL = "https://httpbin.org" private const val GET_UUID = "$BASE_URL/uuid" fun simpleCase() { val client = HttpClient() GlobalScope.launch(Dispatchers.IO) { val data = client.get<String>(GET_UUID) Log.i("$BASE_TAG Simple case ", data) } } 

Vous pouvez créer un client sans paramètres, il suffit de créer une instance de HttpClient() . Dans ce cas, Ktor sélectionnera le moteur souhaité et l'utilisera avec les paramètres par défaut (nous avons un moteur connecté - Android, mais il y en a d'autres, par exemple, OkHttp).
Pourquoi les coroutines? Parce que get() est une fonction de suspend .

Que faire ensuite? Vous avez déjà des données du serveur sous forme de chaîne, il suffit de les analyser et d'obtenir des classes avec lesquelles vous pouvez déjà travailler. Il semble être simple et rapide dans ce cas d'utilisation.

Nous obtenons une réponse brute


Parfois, il peut être nécessaire d'obtenir un ensemble d'octets au lieu d'une chaîne. En même temps, expérimentez l'asynchronie.

 fun performAllCases() { GlobalScope.launch(Dispatchers.IO) { simpleCase() bytesCase() } } suspend fun simpleCase() { val client = HttpClient() val data = client.get<String>(GET_UUID) Log.i("$BASE_TAG Simple case", data) } suspend fun bytesCase() { val client = HttpClient() val data = client.call(GET_UUID).response.readBytes() Log.i("$BASE_TAG Bytes case", data.joinToString(" ", "[", "]") { it.toString(16).toUpperCase() }) } 

Aux endroits où les méthodes HttpClient sont HttpClient , comme call() et get() , await() sera appelé sous le capot. Dans ce cas, les appels à simpleCase() et bytesCase() seront donc toujours séquentiels. Vous en avez besoin en parallèle - il suffit d'envelopper chaque appel dans une coroutine distincte. Dans cet exemple, de nouvelles méthodes sont apparues. L'appel call(GET_UUID) retournera un objet à partir duquel nous pouvons obtenir des informations sur la demande, sa configuration, sa réponse et son client. L'objet contient de nombreuses informations utiles - du code de réponse et de la version du protocole au canal avec les mêmes octets.

Avez-vous besoin de le fermer d'une manière ou d'une autre?


Les développeurs indiquent que pour que le moteur HTTP s'arrête correctement, vous devez appeler la méthode close() sur le client. Si vous devez effectuer un appel et fermer immédiatement le client, vous pouvez utiliser la méthode use{} , car HttpClient implémente l'interface Closable .

 suspend fun closableSimpleCase() { HttpClient().use { val data: String = it.get(GET_UUID) Log.i("$BASE_TAG Closable case", data) } } 

Exemples en plus de GET


Dans mon travail, la deuxième méthode la plus populaire est le POST . Prenons l'exemple de la définition des paramètres, des en-têtes et du corps de la demande.

 suspend fun postHeadersCase(client: HttpClient) { val data: String = client.post(POST_TEST) { fillHeadersCaseParameters() } Log.i("$BASE_TAG Post case", data) } private fun HttpRequestBuilder.fillHeadersCaseParameters() { parameter("name", "Andrei") // +     url.parameters.appendAll( parametersOf( "ducks" to listOf("White duck", "Grey duck"), // +      "fish" to listOf("Goldfish") // +     ) ) header("Ktor", "https://ktor.io") // +  headers /*       */ { append("Kotlin", "https://kotl.in") } headers.append("Planet", "Mars") // +  headers.appendMissing("Planet", listOf("Mars", "Earth")) // +   , "Mars"   headers.appendAll("Pilot", listOf("Starman")) //     body = FormDataContent( //  ,     form Parameters.build { append("Low-level", "C") append("High-level", "Java") } ) } 

En fait, dans le dernier paramètre de la post() , vous avez accès à HttpRequestBuilder , avec lequel vous pouvez former n'importe quelle demande.
La méthode post() analyse simplement la chaîne, la convertit en URL, définit explicitement le type de la méthode et fait une demande.

 suspend fun rawPostHeadersCase(client: HttpClient) { val data: String = client.call { url.takeFrom(POST_TEST) method = HttpMethod.Post fillHeadersCaseParameters() } .response .readText() Log.i("$BASE_TAG Raw post case", data) } 

Si vous exécutez le code des deux dernières méthodes, le résultat sera similaire. La différence n'est pas grande, mais l'utilisation de wrappers est plus pratique. La situation est similaire pour put() , delete() , patch() , head() et options() , nous ne les considérerons donc pas.

Cependant, si vous regardez attentivement, vous pouvez voir qu'il y a une différence de frappe. Lorsque vous appelez call() vous obtenez une réponse de bas niveau et vous devez lire les données vous-même, mais qu'en est-il de la saisie automatique? Après tout, nous sommes tous habitués à connecter un convertisseur (tel que Gson ) dans Retrofit2 et à indiquer le type de retour en tant que classe spécifique. Nous parlerons de la conversion en classes plus tard, mais la méthode request aidera à taper le résultat sans se lier à une méthode HTTP spécifique.

 suspend fun typedRawPostHeadersCase(client: HttpClient) { val data = client.request<String>() { url.takeFrom(POST_TEST) method = HttpMethod.Post fillHeadersCaseParameters() } Log.i("$BASE_TAG Typed raw post", data) } 

Soumettre les données du formulaire


Habituellement, vous devez passer des paramètres dans la chaîne de requête ou dans le corps. Dans l'exemple ci-dessus, nous avons déjà examiné comment procéder à l'aide de HttpRequestBuilder . Mais cela peut être plus simple.

La fonction submitForm accepte l'url en tant que chaîne, les paramètres de la demande et un indicateur booléen qui indique comment transmettre les paramètres - dans la ligne de demande ou sous forme de paires dans le formulaire.

 suspend fun submitFormCase(client: HttpClient) { val params = Parameters.build { append("Star", "Sun") append("Planet", "Mercury") } val getData: String = client.submitForm(GET_TEST, params, encodeInQuery = true) //     val postData: String = client.submitForm(POST_TEST, params, encodeInQuery = false) //   form Log.i("$BASE_TAG Submit form get", getData) Log.i("$BASE_TAG Submit form post", postData) } 

Mais qu'en est-il des multipart / form-data?


En plus des paires de chaînes, vous pouvez passer comme paramètres des numéros de demande POST, des tableaux d'octets et divers flux d'entrée. Différences dans la formation des fonctions et des paramètres. Nous ressemblons à:

 suspend fun submitFormBinaryCase(client: HttpClient) { val inputStream = ByteArrayInputStream(byteArrayOf(77, 78, 79)) val formData = formData { append("String value", "My name is") //   append("Number value", 179) //  append("Bytes value", byteArrayOf(12, 74, 98)) //   append("Input value", inputStream.asInput(), headersOf("Stream header", "Stream header value")) //    } val data: String = client.submitFormWithBinaryData(POST_TEST, formData) Log.i("$BASE_TAG Submit binary case", data) } 

Comme vous l'avez peut-être remarqué - vous pouvez également attacher un ensemble d'en-têtes à chaque paramètre.

Désérialiser la réponse à la classe


Vous devez obtenir certaines données de la demande, pas sous forme de chaîne ou d'octets, mais immédiatement converties en classe. Pour commencer, dans la documentation, nous recommandons de connecter une fonctionnalité pour travailler avec json, mais je veux faire une réservation pour que jvm ait besoin d'une dépendance spécifique et sans kotlinx-sérialisation, tout cela ne décollera pas. Je suggère d'utiliser Gson comme convertisseur (il y a des liens vers d'autres bibliothèques prises en charge dans la documentation, des liens vers la documentation seront à la fin de l'article).

niveau du projet build.gradle:

 buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } allprojects { repositories { maven { url "https://kotlin.bintray.com/kotlinx" } } } 

Niveau d'application build.gradle:

 apply plugin: 'kotlinx-serialization' dependencies { implementation "io.ktor:ktor-client-json-jvm:1.0.1" implementation "io.ktor:ktor-client-gson:1.0.1" } 

Exécutez maintenant la demande. De la nouvelle, il n'y aura qu'une connexion de la fonctionnalité de travailler avec Json lors de la création du client. J'utiliserai l'API Open Weather. Pour être complet, je vais montrer le modèle de données.

 data class Weather( val consolidated_weather: List<ConsolidatedWeather>, val time: String, val sun_rise: String, val sun_set: String, val timezone_name: String, val parent: Parent, val sources: List<Source>, val title: String, val location_type: String, val woeid: Int, val latt_long: String, val timezone: String ) data class Source( val title: String, val slug: String, val url: String, val crawl_rate: Int ) data class ConsolidatedWeather( val id: Long, val weather_state_name: String, val weather_state_abbr: String, val wind_direction_compass: String, val created: String, val applicable_date: String, val min_temp: Double, val max_temp: Double, val the_temp: Double, val wind_speed: Double, val wind_direction: Double, val air_pressure: Double, val humidity: Int, val visibility: Double, val predictability: Int ) data class Parent( val title: String, val location_type: String, val woeid: Int, val latt_long: String ) private const val SF_WEATHER_URL = "https://www.metaweather.com/api/location/2487956/" suspend fun getAndPrintWeather() { val client = HttpClient(Android) { install(JsonFeature) { serializer = GsonSerializer() } } val weather: Weather = client.get(SF_WEATHER_URL) Log.i("$BASE_TAG Serialization", weather.toString()) } 

Et quoi d'autre peut


Par exemple, le serveur renvoie une erreur et vous disposez du code comme dans l'exemple précédent. Dans ce cas, vous recevrez une erreur de sérialisation, mais vous pouvez configurer le client de sorte qu'une erreur BadResponseStatus soit levée lorsque le code de réponse est <300. Il suffit de définir expectSuccess sur true lors de la création du client.

  val client = HttpClient(Android) { install(JsonFeature) { serializer = GsonSerializer() } expectSuccess = true } 

Lors du débogage, la journalisation peut être utile. Ajoutez simplement une dépendance et configurez le client.

 implementation "io.ktor:ktor-client-logging-jvm:1.0.1" 

  val client = HttpClient(Android) { install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL } } 

Nous spécifions l'enregistreur PAR DÉFAUT et tout ira à LogCat, mais vous pouvez redéfinir l'interface et créer votre propre enregistreur si vous le souhaitez (bien que je n'y ai pas vu de grandes opportunités, il n'y a qu'un message à l'entrée, mais il n'y a pas de niveau de journal). Nous indiquons également le niveau de journaux à refléter.

Références:


Ce qui n'est pas considéré:

  • Travailler avec le moteur OkHttp
  • Paramètres du moteur
  • Moteur simulé et tests
  • Module d'autorisation
  • Fonctionnalités distinctes telles que le stockage des cookies entre les demandes, etc.
  • Tout ce qui ne s'applique pas au client HTTP pour Android (autres plates-formes, travail via des sockets, implémentation de serveur, etc.

Source: https://habr.com/ru/post/fr432310/


All Articles