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