Ktor als HTTP-Client für Android

Ich mag Retrofit2 als Android-Entwickler, aber was ist mit dem Versuch, die Qualität des Ktor-HTTP-Clients zu erhalten? Meiner Meinung nach ist es für die Android-Entwicklung nicht schlechter und nicht besser, nur eine der Optionen, obwohl es sehr gut ausgehen kann, wenn Sie alles ein wenig zusammenfassen. Ich werde die grundlegenden Funktionen betrachten, mit denen Ktor als HTTP-Client verwendet werden kann: Erstellen verschiedener Arten von Anforderungen, Empfangen von Rohantworten und Antworten in Form von Text, Deserialisieren von JSON in Klassen über Konverter und Protokollieren.



Im Allgemeinen ist Ktor ein Framework, das als HTTP-Client fungieren kann. Ich werde es von der Entwicklungsseite für Android betrachten. Es ist unwahrscheinlich, dass Sie im Folgenden sehr komplexe Anwendungsfälle sehen, aber die grundlegenden Funktionen sind sicher. Der Code aus den folgenden Beispielen kann auf GitHub angezeigt werden.

Ktor verwendet die Coroutinen aus Kotlin 1.3, eine Liste der verfügbaren Artefakte finden Sie hier , die aktuelle Version ist 1.0.1 .
Für Anfragen werde ich HttpBin verwenden .

Einfache Verwendung


Für den Einstieg benötigen Sie grundlegende Abhängigkeiten für den Android-Client:

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

Vergessen Sie nicht, Manifest Informationen hinzuzufügen, die Sie über das Internet verwenden.

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

Versuchen wir, die Serverantwort als Zeichenfolge abzurufen. Was könnte einfacher sein?

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

Sie können einen Client ohne Parameter erstellen. Erstellen Sie einfach eine Instanz von HttpClient() . In diesem Fall wählt Ktor die gewünschte Engine aus und verwendet sie mit den Standardeinstellungen (wir haben eine Engine angeschlossen - Android, aber es gibt andere, zum Beispiel OkHttp).
Warum Coroutinen? Weil get() eine suspend Funktion ist.

Was kann als nächstes getan werden? Sie haben bereits Daten vom Server in Form einer Zeichenfolge. Es reicht aus, diese zu analysieren und Klassen abzurufen, mit denen Sie bereits arbeiten können. In diesem Fall scheint es einfach und schnell zu sein.

Wir bekommen eine rohe Antwort


Manchmal kann es erforderlich sein, eine Reihe von Bytes anstelle einer Zeichenfolge abzurufen. Experimentieren Sie gleichzeitig mit Asynchronität.

 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() }) } 

An Stellen, an denen HttpClient Methoden HttpClient , wie z. B. call() und get() , wird await() unter der Haube aufgerufen. In diesem Fall sind die Aufrufe von simpleCase() und bytesCase() immer sequentiell. Sie benötigen es parallel - wickeln Sie einfach jeden Anruf in eine separate Coroutine. In diesem Beispiel sind neue Methoden erschienen. call(GET_UUID) Sie call(GET_UUID) wird ein Objekt zurückgegeben, von dem wir Informationen über die Anforderung, ihre Konfiguration, Antwort und den Client erhalten können. Das Objekt enthält viele nützliche Informationen - vom Antwortcode und der Protokollversion bis zum Kanal mit denselben Bytes.

Müssen Sie es irgendwie schließen?


Die Entwickler geben an, dass Sie die Methode close() auf dem Client aufrufen müssen, damit die HTTP-Engine ordnungsgemäß heruntergefahren wird. Wenn Sie einen Aufruf tätigen und den Client sofort schließen müssen, können Sie die Methode use{} verwenden, da HttpClient die Closable Schnittstelle implementiert.

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

Beispiele neben GET


In meiner Arbeit ist POST die zweitbeliebteste Methode. Betrachten Sie das Beispiel zum Festlegen von Parametern, Headern und Anforderungshauptteilen.

 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") } ) } 

Tatsächlich haben Sie im letzten Parameter der Funktion post() Zugriff auf den HttpRequestBuilder , mit dem Sie eine beliebige Anforderung bilden können.
Die post() -Methode analysiert einfach die Zeichenfolge, konvertiert sie in eine URL, legt den Methodentyp explizit fest und stellt eine Anforderung.

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

Wenn Sie den Code der letzten beiden Methoden ausführen, ist das Ergebnis ähnlich. Der Unterschied ist nicht groß, aber die Verwendung von Wrappern ist bequemer. Die Situation ist für put() , delete() , patch() , head() und options() ähnlich, daher werden wir sie nicht berücksichtigen.

Wenn Sie jedoch genau hinschauen, können Sie feststellen, dass es einen Unterschied bei der Eingabe gibt. Wenn Sie call call() Sie eine Antwort auf niedriger Ebene und müssen die Daten selbst lesen. Was ist jedoch mit der automatischen Eingabe? Schließlich sind wir alle daran Gson einen Konverter (wie Gson ) in Retrofit2 Gson und den Rückgabetyp als bestimmte Klasse anzugeben. Wir werden später über die Konvertierung in Klassen sprechen, aber die request hilft dabei, das Ergebnis zu typisieren, ohne an eine bestimmte HTTP-Methode gebunden zu sein.

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

Senden Sie Formulardaten


Normalerweise müssen Sie Parameter entweder in der Abfragezeichenfolge oder im Text übergeben. Im obigen Beispiel haben wir bereits untersucht, wie dies mit dem HttpRequestBuilder . Aber es kann einfacher sein.

Die Funktion submitForm akzeptiert die URL als Zeichenfolge, Parameter für die Anforderung und ein boolesches Flag, das submitForm , wie Parameter übergeben werden sollen - in der Anforderungszeile oder als Paare im Formular.

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

Aber was ist mit mehrteiligen / Formulardaten?


Zusätzlich zu Zeichenfolgenpaaren können Sie als Parameter POST-Anforderungsnummern, Bytearrays und verschiedene Eingabestreams übergeben. Unterschiede in Funktion und Parameterbildung. Wir sehen aus wie:

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

Wie Sie vielleicht bemerkt haben, können Sie jedem Parameter auch eine Reihe von Headern hinzufügen.

Deserialisieren Sie die Antwort auf die Klasse


Sie müssen einige Daten aus der Anforderung abrufen, nicht als Zeichenfolge oder Bytes, sondern sofort in eine Klasse konvertieren. Zunächst empfehlen wir in der Dokumentation, eine Funktion für die Arbeit mit json anzuschließen. Ich möchte jedoch reservieren, dass jvm eine bestimmte Abhängigkeit benötigt und dass dies ohne Kotlinx-Serialisierung nicht funktioniert. Ich schlage vor, Gson als Konverter zu verwenden (es gibt Links zu anderen unterstützten Bibliotheken in der Dokumentation, Links zur Dokumentation finden Sie am Ende des Artikels).

build.gradle Projektebene:

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

build.gradle-Anwendungsebene:

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

Führen Sie nun die Anfrage aus. Ab dem neuen gibt es nur eine Verbindung der Funktion der Arbeit mit Json beim Erstellen des Clients. Ich werde die Open-Weather-API verwenden. Der Vollständigkeit halber werde ich das Datenmodell zeigen.

 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()) } 

Und was kann noch


Beispielsweise gibt der Server einen Fehler zurück und Sie haben den Code wie im vorherigen Beispiel. In diesem Fall wird ein Serialisierungsfehler BadResponseStatus Sie können den Client jedoch so konfigurieren, dass ein BadResponseStatus Fehler ausgelöst wird, wenn der Antwortcode <300 ist. Es reicht aus, expectSuccess beim expectSuccess des Clients auf true zu setzen.

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

Beim Debuggen kann die Protokollierung hilfreich sein. Fügen Sie einfach eine Abhängigkeit hinzu und konfigurieren Sie den Client.

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

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

Wir geben den DEFAULT-Logger an und alles wird an LogCat gesendet. Sie können jedoch die Benutzeroberfläche neu definieren und Ihren eigenen Logger erstellen, wenn Sie dies wünschen (obwohl ich dort keine großen Möglichkeiten gesehen habe, gibt es nur eine Nachricht am Eingang, aber keine Log-Ebene). Wir geben auch die Ebene der Protokolle an, die reflektiert werden müssen.

Referenzen:


Was nicht berücksichtigt wird:

  • Arbeiten Sie mit der OkHttp-Engine
  • Motoreinstellungen
  • Scheinmotor und Prüfung
  • Autorisierungsmodul
  • Separate Funktionen wie das Speichern von Cookies zwischen Anfragen usw.
  • Alles, was nicht für den HTTP-Client für Android gilt (andere Plattformen, Sockets, Serverimplementierung usw.)

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


All Articles