Ktor como cliente HTTP para Android

Me gusta Retrofit2 como desarrollador de Android, pero ¿qué hay de tratar de obtener la calidad del cliente Ktor HTTP? En mi opinión, para el desarrollo de Android no es peor ni mejor, solo una de las opciones, aunque si lo terminas un poco, puede salir muy bien. Consideraré las características básicas con las que será posible comenzar a usar Ktor como cliente HTTP: crear varios tipos de solicitudes, recibir respuestas sin formato y respuestas en forma de texto, deserializar json en clases a través de convertidores e iniciar sesión.



En general, Ktor es un marco que puede actuar como un cliente HTTP. Lo consideraré desde el lado del desarrollo para Android. Es poco probable que vea a continuación casos de uso muy complejos, pero las características básicas son seguras. El código de los ejemplos a continuación se puede ver en GitHub .

Ktor usa las corutinas de Kotlin 1.3, una lista de artefactos disponibles se puede encontrar aquí , la versión actual es 1.0.1 .
Para consultas, usaré HttpBin .

Uso simple


Para comenzar, necesitará dependencias básicas para el cliente de Android:

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

No olvide agregar información al Manifiesto de que usa Internet.

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

Intentemos obtener la respuesta del servidor como una cadena, ¿qué podría ser más fácil?

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

Puede crear un cliente sin parámetros, simplemente cree una instancia de HttpClient() . En este caso, Ktor seleccionará el motor deseado y lo usará con la configuración predeterminada (tenemos un motor conectado: Android, pero hay otros, por ejemplo, OkHttp).
¿Por qué corutinas? Porque get() es una función de suspend .

¿Qué se puede hacer a continuación? Ya tiene datos del servidor en forma de cadena, es suficiente analizarlos y obtener clases con las que ya puede trabajar. Parece ser simple y rápido en este caso de uso.

Obtenemos una respuesta cruda


A veces puede ser necesario obtener un conjunto de bytes en lugar de una cadena. Al mismo tiempo, experimente con asincronía.

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

En los lugares donde se HttpClient métodos HttpClient , como call() y get() , se llamará a await() debajo del capó. Entonces, en este caso, las llamadas a simpleCase() y bytesCase() siempre serán secuenciales. Lo necesita en paralelo: simplemente envuelva cada llamada en una rutina separada. En este ejemplo, han aparecido nuevos métodos. Llamar a call(GET_UUID) devolverá un objeto del que podemos obtener información sobre la solicitud, su configuración, respuesta y cliente. El objeto contiene mucha información útil, desde el código de respuesta y la versión del protocolo hasta el canal con los mismos bytes.

¿Necesitas cerrarlo de alguna manera?


Los desarrolladores indican que para que el motor HTTP se apague correctamente, debe llamar al método close() en el cliente. Si necesita hacer una llamada e inmediatamente cerrar el cliente, puede usar el método use{} , ya que HttpClient implementa la interfaz Closable .

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

Ejemplos además de GET


En mi trabajo, el segundo método más popular es POST . Considere el ejemplo de establecer parámetros, encabezados y cuerpo de solicitud.

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

De hecho, en el último parámetro de la función post() , tiene acceso al HttpRequestBuilder , con el que puede formular cualquier solicitud.
El método post() simplemente analiza la cadena, la convierte en una URL, establece explícitamente el tipo del método y realiza una solicitud.

 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 ejecuta el código de los dos últimos métodos, el resultado será similar. La diferencia no es grande, pero usar envoltorios es más conveniente. La situación es similar para put() , delete() , patch() , head() y options() , por lo que no los consideraremos.

Sin embargo, si observa de cerca, puede ver que hay una diferencia en la escritura. Cuando llama a call() obtiene una respuesta de bajo nivel y debe leer los datos usted mismo, pero ¿qué pasa con la escritura automática? Después de todo, todos estamos acostumbrados a conectar un convertidor (como Gson ) en Retrofit2 e indicar el tipo de retorno como una clase específica. Hablaremos sobre la conversión a clases más tarde, pero el método de request ayudará a tipificar el resultado sin vincularlo a un método HTTP específico.

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

Enviar datos del formulario


Por lo general, debe pasar los parámetros en la cadena de consulta o en el cuerpo. En el ejemplo anterior, ya hemos examinado cómo hacer esto usando HttpRequestBuilder . Pero puede ser más fácil.

La función submitForm acepta la url como una cadena, parámetros para la solicitud y un indicador booleano que indica cómo pasar parámetros, en la línea de solicitud o como pares en el formulario.

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

Pero, ¿qué pasa con multipart / form-data?


Además de los pares de cadenas, puede pasar como parámetros números de solicitud POST, conjuntos de bytes y varias secuencias de entrada. Diferencias en función y formación de parámetros. Nos vemos como:

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

Como habrás notado, también puedes adjuntar un conjunto de encabezados a cada parámetro.

Deserializar la respuesta a la clase.


Necesita obtener algunos datos de la solicitud, no como una cadena o bytes, sino que se convierte inmediatamente en una clase. Para empezar, en la documentación recomendamos conectar una función para trabajar con json, pero quiero hacer una reserva de que jvm necesita una dependencia específica y sin la serialización de kotlinx, todo esto no despegará. Sugiero usar Gson como convertidor (hay enlaces a otras bibliotecas compatibles en la documentación, los enlaces a la documentación estarán al final del artículo).

Nivel del proyecto build.gradle:

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

Nivel de aplicación 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" } 

Ahora ejecuta la solicitud. A partir de lo nuevo, solo habrá una conexión de la función de trabajar con Json al crear el cliente. Usaré la API de clima abierto. Para completar, mostraré el modelo de datos.

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

Y que mas puede


Por ejemplo, el servidor devuelve un error y tiene el código como en el ejemplo anterior. En este caso, recibirá un error de serialización, pero puede configurar el cliente para que se BadResponseStatus un error BadResponseStatus cuando el código de respuesta sea <300. Es suficiente establecer expectSuccess en true al construir el cliente.

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

Al depurar, el registro puede ser útil. Simplemente agregue una dependencia y configure el cliente.

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

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

Especificamos el registrador PREDETERMINADO y todo irá a LogCat, pero puede redefinir la interfaz y crear su propio registrador si lo desea (aunque no vi grandes oportunidades allí, solo hay un mensaje en la entrada, pero no hay un nivel de registro). También indicamos el nivel de registros que deben reflejarse.

Referencias


Lo que no se considera:

  • Trabaja con el motor OkHttp
  • Configuraciones del motor
  • Simulacro de motor y pruebas
  • Módulo de autorización
  • Funciones separadas como el almacenamiento de cookies entre solicitudes, etc.
  • Todo lo que no se aplica al cliente HTTP para Android (otras plataformas, trabajo a través de sockets, implementación de servidor, etc.

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


All Articles