Table of Contents
Introducción
La comunicación entre las aplicaciones de Android y los servidores es un aspecto crítico en el desarrollo de aplicaciones, y elegir la herramienta adecuada para realizar esta comunicación es fundamental. Hasta ahora, Retrofit ha sido el estándar por excelencia. Sin embargo, en los últimos años ha surgido una alternativa interesante: Ktor Client.
Esta librería, desarrollada por el equipo de JetBrains completamente en Kotlin, ofrece una forma más moderna y flexible de realizar peticiones HTTP sin depender de anotaciones, con una sintaxis más sencilla y fácil de entender. Sus plugins, como la autenticación y la serialización existen como dependencias externas, por lo que se pueden instalar solo los que necesites, lo que la hace una librería ligera. Además, no menos importante, es multiplataforma.
En este artículo veremos ejemplos prácticos que comparan ambos clientes y ofrecen una visión general para migrar cualquier proyecto. ¡Empecemos! Nota: en este artículo se asume que el lector tiene conocimientos básicos de Retrofit, por lo que no entraremos en detalle en esta librería.
Caso base
Imaginemos el escenario en el que tenemos esta configuración básica de Retrofit: utilizamos Gson Converter, envolvemos la respuesta del servidor en Kotlin Result y registramos la interacción HTTP mediante un interceptor.
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.github.skydoves:retrofit-adapters-result:$retrofit_result_version" implementation "com.squareup.okhttp3:logging-interceptor:$logging_interceptor_version"
// creates okHttp engine with logger interceptor
val okHttp = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor()
.apply {
setLevel(
HttpLoggingInterceptor.Level.BODY
)
})
.build()
val retrofit = Retrofit.Builder()
// add base url for all request
.baseUrl(BASE_URL)
// add gson content negotiation
.addConverterFactory(GsonConverterFactory.create())
// add adapter to wrap response with result
.addCallAdapterFactory(ResultCallAdapterFactory.create())
.client(okHttp)
.build()
¿Cómo configurar Ktor?
Para migrar a Ktor necesitaríamos estas dependencias:
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
implementation "io.ktor:ktor-serialization-gson:$ktor_version"
implementation "com.squareup.okhttp3:logging-interceptor:$logging_interceptor_version"
ktor-client-core
librería principal de Ktor, incluye plugins interesantes como HttpRequestRetry, HttpSend, y HttpCache, entre otros.ktor-client-okhttp
motor para procesar las solicitudes de red. Existen varios motores compatibles con android: OkHttp ( utilizado en Retrofit, soporta Http/2 y WebSockets), *CIO (*implementación basada en corutinas ) y Android ( motor que soporta versiones antiguas de Android 1.x+).ktor-client-content-negotiation
plugin para serializar/deserializar contenido.ktor-serialization-gson
serializador JSON. Existen otras alternativas oficiales como Jackson y Kotlin serialization.
Una vez agregadas las librerías la configuración de Ktor equivalente sería:
// creates Ktor client with OkHttp engine
val Ktor = HttpClient(OkHttp) {
// default validation to throw exceptions for non-2xx responses
expectSuccess = true
engine {
// add logging interceptor
addInterceptor(HttpLoggingInterceptor().apply {
setLevel(
HttpLoggingInterceptor.Level.BODY
)
})
}
// set default request parameters
defaultRequest {
// add base url for all request
url(BASE_URL)
}
// use gson content negotiation for serialize or deserialize
install(ContentNegotiation) {
gson()
}
}
Utilizar el engine de OkHttp nos permite reutilizar los interceptores que tengamos creados en nuestro proyecto.
¿Cómo crear el ApiService?
En Retrofit, definimos una interfaz y cada método debe tener una anotación HTTP que proporcione el método de solicitud y la librería se encarga de generar el código en tiempo de compilación. Mientras Ktor proporciona funciones equivalentes a estas anotaciones y podemos reutilizar la interfaz existente (eliminando las anotaciones de retrofit ) para no romper el contrato, somos nosotros los encargados de definir la implementación de estas peticiones.
// Retrofit create a service
val service = retrofit.create<UserApiService>()
—-------—-------—-------—-------—-------—-------—-------—-------
// Ktor create a service
val service: UserApiService = UserApiServiceImpl(Ktor)
¿Cómo realizar peticiones?
Ahora que ya tenemos configurado el cliente veamos unos ejemplos sobre cómo hacer una petición GET, como manipular la URL reemplazando bloques con Path y la forma de agregar parámetros Query o Cabeceras, junto con las diferentes formas de enviar datos.
Peticiones GET – Manipulación de URL
- Retrofit
interface UserApiService {
@GET("/user")
suspend fun getUsers(): Result<Users>
// Replacements blocks
@GET("/user/{id}")
suspend fun getUserById(@Path("id") id: Int): Result<User>
// Query parameter
@GET("/user")
suspend fun getUsers(@Query("page") page: Int): Result<Users>
// Complex query combinations
@GET("/user")
suspend fun getUsers(@QueryMap queries: Map<String, String>): Result<Users>
}
- Ktor
class UserApiServiceImpl(private val Ktor: HttpClient) : UserApiService {
suspend fun getUsers(): Result<Users> = runCatching {
Ktor.get("/user").body()
}
// Replacements blocks
suspend fun getUserById(id: Int): Result<User> = runCatching {
Ktor.get("/user/$id").body()
}
// Query parameters
suspend fun getUsers(page: Int): Result<Users> = runCatching {
Ktor.get("/user") {
parameter("page", page)
}.body()
}
// Complex query combinations
suspend fun getUsers(queries: Map<String, String>): Result<Users> = runCatching {
Ktor.get("/user") {
queries.forEach {
parameter(it.key, it.value)
}
}.body()
}
}
Peticiones POST – Envío de datos
- Retrofit
// request body
@POST("/user")
suspend fun createUser(@Body user: User): Result<User>
// request using x-www-form-urlencoded
@FormUrlEncoded
@POST("/user/edit")
suspend fun editUser(
@Field("first_name") first: String,
@Field("last_name") last: String
): Result<User>
// request multi-part
@Multipart
@POST("user/update")
suspend fun updateUser(@Part("photo") photo: MultipartBody.Part): Result<User>
- Ktor
// request body
suspend fun createUser(user: User): Result<User> = runCatching {
Ktor.post("/user") { setBody(user) }.body()
}
// request using x-www-form-urlencoded
suspend fun editUser(first: String, last: String): Result<User> = runCatching {
Ktor.submitForm(
url = "/user/edit",
formParameters = Parameters.build {
append("first_name", first)
append("last_name", last)
}
).body()
}
// request multi-part
suspend fun updateUser(photo: String): Result<User> = runCatching {
Ktor.post("/user/update") {
setBody(MultiPartFormDataContent(
formData {
append("photo", File(photo).readBytes())
}
))
}.body()
}
Headers
- Retrofit
@Headers("Cache-Control: max-age=640000")
@GET("/user")
suspend fun getUsers(): Result<Users>
@Headers({
"Accept: application/vnd.github.v3.full+json",
"User-Agent: Retrofit-App"
})
@GET("/user")
suspend fun getUsers(): Result<Users>
@GET("/user")
suspend fun getUsers(@Header("Authorization") auth: String): Result<Users>
- Ktor
suspend fun getUsers(): Result<Users> = runCatching {
Ktor.get("/user") {
headers {
append("header", "value")
append("header", "value1") // this value will be appended to previous
appendIfNameAbsent("header", "value2") // this won't be added as name exists
appendIfNameAndValueAbsent("header2", "value1") // this won't be added
}
}.body()
}
Conclusión
Resumiendo, la implementación de Ktor Client es un poco más verbosa pero al mismo tiempo es fácil de entender y altamente configurable. Por ejemplo, podemos tipear la URL, que al tener las peticiones de manera estructurada nos facilita el mantenimiento, proporcionándonos a la vez mayor control/seguridad sobre los tipos utilizados. También nos permite crear custom plugins que entre otras cosas nos facilita interceptar las llamadas.
Y si, es un poco engorroso envolver la llamada cada vez en un unCatching
o hacer .body()
para obtener el payload de la respuesta pero con esta pequeña extensión evitamos repetirnos.
suspend inline fun <reified R> HttpClient.getResult(
urlString: String,
builder: HttpRequestBuilder.() -> Unit = {}
): Result<R> = runCatching { get(urlString, builder).body() }
¿Te interesa aprender más sobre diferentes temas de desarrollo Android? Te sugiero que eches un vistazo al blog de Apiumhub. Allí encontrarás artículos sobre las últimas tecnologías móviles, así como sobre desarrollo frontend, desarrollo backend y arquitectura de software.
Author
-
Android Passionate Developer that enjoy solving issues. Focused on developing quality and scalable software.
Ver todas las entradas