Migrando Retrofit a Ktor

Compartir esta publicación

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.
  iOS Snapshot tests

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()
}
¿Tienes un proyecto de Android en mente? ¡Contáctanos!

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.

  Nuestra experiencia migrando de Dagger a Koin

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

¿Tienes un proyecto desafiante?

Podemos trabajar juntos

apiumhub software development projects barcelona
Secured By miniOrange