Como desarrolladores Mobile/Multiplataforma una de las tareas más habituales que haremos en el día a día es el consumo de APIs para poder pintar información en nuestras aplicaciones. En este capítulo aprenderemos a consumir servicios REST en Kotlin Multiplatform con la librería Ktor.

Como este curso es más intermedio, si todavía no sabes lo que son las APIs o servicios REST te recomiendo ver este capítulo antes de continuar.

Ktor Client en Kotlin Multiplatform

Si vienes del desarrollo Android, lo más probable es que trabajes asiduamente con Retrofit, pues Ktor viene a ser lo mismo, un cliente que nos ayuda y simplifica el consumo de datos.

La principal ventaja de esta librería es que está hecha completamente en Kotlin con corrutinas por lo que la integración es pan comido, además está creada por JetBrains, los creadores de Kotlin y Android Studio.

Web Ktor.io

Ktor no solo actúa de cliente, sino que también lo podemos usar en modo servidor para la creación de entornos backend aunque lo dejaremos para otro curso.

Programando nuestra app con KMP y Ktor

Para este proyecto iremos al wizard de JetBrains y crearemos un proyecto desde cero, con Android y iOS con shared UI (ya que haremos el proyecto entero con Jetpack Compose, es decir Compose Multiplatform). Recuerda que tienes todo el código del proyecto en el Curso de Kotlin Multiplatform de Github.

Configuración proyecto kmp ktor
Configuración del proyecto de KMP con Ktor.

Preparando las dependencias

Una vez creado y abierto el proyecto en Android Studio tendremos que añadir ciertas dependencias para poder hacer funcionar todo el proyecto.

El primer paso será poner vista en modo Project, no es obligatorio (es solo una representación visual) pero si quieres que las rutas encajen con las de este tutorial tendrás que hacerlo. Una vez seleccionado tendremos que ir al fichero build.gradle.kts del módulo ComposeApp.

ruta dependencias kmp
Ruta del fichero de las dependencias en KMP.

Dentro de ese fichero veremos la etiqueta sourceSets{} la cual contiene las dependencias la selección de dependencias por plataforma.

Es decir, aunque ahí no vayan las dependencias como tal, tenemos que ver que por defecto nos trae androidMain.dependencies{} y commonMain.dependencies{} esas son las rutas para poner librerías específicas de Android o librerías genéricas independientemente de la plataforma.

Si queremos poder añadir librerías específicas para iOS tendremos que crear otra función llamada iosMain.dependencies{} y sincronizar.

Ahora iremos al directorio gradle/libs.versions.toml (Puedes ver la ruta en la imagen anterior) y será el lugar donde tengamos que añadir todas las dependencias ya que el proyecto viene con VersionCatalogs.

En la parte de [versions] añadiremos la versión de Ktor. A día de hoy la última es la 2.3.11.

ktor = "2.3.11"

También tendremos que añadir en [libraries] todas las dependencias de Ktor y la de serialization.

ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
kotlin-serialization = {module = "io.ktor:ktor-serialization-kotlinx-json", version.ref="ktor"}

Terminamos en la parte de [plugins] añadiendo Kotlin Serialization (sí, además de la librería).

kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

La serialización es la función que nos ayudará a pasar los Json a data classes. Fíjate también que el plugin utiliza la librería de Kotlin, en lugar de la de Ktor.

Ahora podemos volver al gradle y añadir las dependencias que hemos importado.

sourceSets {
    
    androidMain.dependencies {
        implementation(compose.preview)
        implementation(libs.androidx.activity.compose)
        implementation(libs.ktor.client.okhttp)
    }
    commonMain.dependencies {
        implementation(compose.runtime)
        implementation(compose.foundation)
        implementation(compose.material)
        implementation(compose.ui)
        implementation(compose.components.resources)
        implementation(compose.components.uiToolingPreview)
        implementation(libs.ktor.client.core)
        implementation(libs.ktor.client.negotiation)
        implementation(libs.kotlin.serialization)
    }
    iosMain.dependencies {
        implementation(libs.ktor.client.darwin)
    }
}

Como puedes observar la serialización, el core y negotiation van en commonMain porque son para todas las plataformas, pero luego iOS utiliza darwin y Android okhttp.

Para terminar, tenemos que aplicar el plugin de serialización (en el paso anterior implementamos solamente la dependencia). Para ello iremos a la parte superior del fichero y dentro de plugins{} añadiremos el nuevo plugin.

    alias(libs.plugins.kotlinxSerialization)

Configurando Ktor

Es el momento de configurar Ktor para nuestro proyecto, para ello, ya que todavía no tenemos arquitectura definida en lo que vamos de curso crearemos un directorio llamado network en la ruta de nuestro proyecto KMP composeApp/src/commonMain/kotlin. Dentro de este nuevo directorio crearemos un object llamado NetworkUtils.

ruta ktor
Ruta del NetworkUtils.
object NetworkUtils {
    val httpClient = HttpClient {
        install(ContentNegotiation){
            json(json = Json { ignoreUnknownKeys = true }, contentType = ContentType.Any)
        }
    }
}

Esta es toda la configuración de Ktor, una configuración del HttpClient la cual recibe una función lambda por parámetro que será la configuración de si misma. Dentro le pasaremos con ContentNegotiation (que no es otra cosa que la configuración genérica que metimos en la dependencia) y al serializador de Json le diremos que ignore las keys que no estemos parseando (con el atributo ignoreUnknownKeys.

SuperHero API

Ahora que tenemos todo configurado es el turno de decidir que datos consumir. En esta ocasión utilizaremos superheroapi, un API gratuita y muy sencilla.

superhero api
SuperHero API.

El único requisito para usarla será generar un access-token gratuito que te dará al hacer login con GitHub.

En este caso utilizaremos el servicio de filtrar por nombres.

https://superheroapi.com/api/access-token/search/name

Lo único que tendremos que hacer será reemplazar access-token por el token que nos ha generado al hacer login y reemplazar name por el texto que queremos filtrar, aunque eso lo haremos de forma automática en nuestro código.

Como es un servicio GET podemos hacer la prueba en el navegador. Si por ejemplo reemplazamos name por superm recibiremos la siguiente respuesta.

{
  "response": "success",
  "results-for": "superm",
  "results": [
    {
      "id": "195",
      "name": "Cyborg Superman",
      "powerstats": {
        "intelligence": "75",
        "strength": "93",
        "speed": "92",
        "durability": "100",
        "power": "100",
        "combat": "80"
      },
      "biography": {
        "full-name": "Henry Henshaw",
        "alter-egos": "No alter egos found.",
        "aliases": [
          "Grandmaster of the Manhunters",
          "Herald of the Anti-Monitor",
          "Alpha-Prime of the Alpha Lanterns"
        ],
        "place-of-birth": "-",
        "first-appearance": "Adventures of Superman #466 (May, 1990)",
        "publisher": "DC Comics",
        "alignment": "bad"
      },
      "appearance": {
        "gender": "Male",
        "race": "Cyborg",
        "height": [
          "-",
          "0 cm"
        ],
        "weight": [
          "- lb",
          "0 kg"
        ],
        "eye-color": "Blue",
        "hair-color": "Black"
      },
      "work": {
        "occupation": "-",
        "base": "Warworld, Qward, Antimatter Universe, formerly Biot, Sector 3601"
      },
      "connections": {
        "group-affiliation": "Alpha Lantern Corps, Manhunters, Warworld, formerly Apokolips and Sinestro Corps",
        "relatives": "Terri Henshaw (wife, deceased)"
      },
      "image": {
        "url": "https://www.superherodb.com/pictures2/portraits/10/100/667.jpg"
      }
    },
    {
      "id": "644",
      "name": "Superman",
      "powerstats": {
        "intelligence": "94",
        "strength": "100",
        "speed": "100",
        "durability": "100",
        "power": "100",
        "combat": "85"
      },
      "biography": {
        "full-name": "Clark Kent",
        "alter-egos": "Superman Prime One-Million",
        "aliases": [
          "Clark Joseph Kent",
          "The Man of Steel",
          "the Man of Tomorrow",
          "the Last Son of Krypton",
          "Big Blue",
          "the Metropolis Marvel",
          "the Action Ace"
        ],
        "place-of-birth": "Krypton",
        "first-appearance": "ACTION COMICS #1",
        "publisher": "Superman Prime One-Million",
        "alignment": "good"
      },
      "appearance": {
        "gender": "Male",
        "race": "Kryptonian",
        "height": [
          "6'3",
          "191 cm"
        ],
        "weight": [
          "225 lb",
          "101 kg"
        ],
        "eye-color": "Blue",
        "hair-color": "Black"
      },
      "work": {
        "occupation": "Reporter for the Daily Planet and novelist",
        "base": "Metropolis"
      },
      "connections": {
        "group-affiliation": "Justice League of America, The Legion of Super-Heroes (pre-Crisis as Superboy); Justice Society of America (pre-Crisis Earth-2 version); All-Star Squadron (pre-Crisis Earth-2 version)",
        "relatives": "Lois Lane (wife), Jor-El (father, deceased), Lara (mother, deceased), Jonathan Kent (adoptive father), Martha Kent (adoptive mother), Seyg-El (paternal grandfather, deceased), Zor-El (uncle, deceased), Alura (aunt, deceased), Supergirl (Kara Zor-El, cousin), Superboy (Kon-El/Conner Kent, partial clone)"
      },
      "image": {
        "url": "https://www.superherodb.com/pictures2/portraits/10/100/791.jpg"
      }
    }
  ]
}

Parseando datos

Ahora que sabemos la respuesta que nos dará el servidor tendremos que crear las clases que Ktor rellenará con la respuesta del endpoint.

Para ello, dentro del directorio network que creamos previamente, crearemos otro directorio llamado model. Ahí crearemos los 2 modelos de datos.

@Serializable
data class ApiResponse(
    val results:List<Hero>,
    @SerialName("response")
    val ok:String
)
@Serializable
data class Hero(
    val id:String,
    val name:String
)

Para que Ktor sea capaz de realizar el parseo de datos, cada modelo tiene que tener la etiqueta @Serializable.

Luego solo tendremos que urilizar el mismo nombre que queremos recuperar del Json en nuestra data class a no ser que utilicemos la etiqueta @SerialName().

Como este ejemplo es sencillo, solo recuperaré un par de datos, ya que voy a representar un listado muy sencillo.

Preparando la vista

La vista será muy sencilla, ya que la finalidad del tutorial es mostrar como se consumen los datos. Si este tutorial tiene mucha repercusión haré una segunda parte mejorando la app.

@Composable
@Preview
fun App() {
    MaterialTheme {
        var superheroName by remember { mutableStateOf("") }
        var superheroList by remember{ mutableStateOf<List<Hero>>(emptyList()) }

        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            Row {
                TextField(value = superheroName, onValueChange = { superheroName = it })
                Button(onClick = { getSuperheroList(superheroName){superheroList = it} }) {
                    Text("Load")
                }
            }
            LazyColumn {
                items(superheroList){ hero ->
                    Text(hero.name)
                }
            }
        }
    }
}

Utilizaremos dos mutableState, el primero será una String que con la ayuda del TextField podrá guardar el nombre del superhéroe que queremos filtrar. Luego disponemos de un listado que contendrá el listado de héroes que nos devuelva backend.

Nos queda la función getSuperheroList() que recibirá por parámetro el nombre a filtrar y devolverá la lista de backend. Esta función se llamará al pulsar el botón de Load.

fun getSuperheroList(superheroName: String, onSuccessResponse: (List<Hero>) -> Unit) {
    if (superheroName.isBlank()) return
    val url =
        "https://www.superheroapi.com/api.php/79c99fda9894cf4017793cdb40721cb6/search/$superheroName"

    CoroutineScope(Dispatchers.IO).launch {
        val response = httpClient.get(url).body<ApiResponse>()
        onSuccessResponse(response.results)
    }
}

Lo primero que haremos es comprobar si el nombre del héroe no es blank, si es correcto añadiremos el nombre al final de la url.

Luego creamos una corrutina y utilizaremos el httpClient que creamos previamente para consumir el API.

Recuerda que puedes descargar el código del proyecto desde Github (y de paso dale una estrella ⭐️)

Curso PREMIUM

Si quieres DOMINAR Kotlin Multiplatform esta es la mejor opción. El curso más completo de habla hispana donde veremos Kotlin Multiplatform, Compose Multiplatform, arquitectura, inyección de dependencias, buenas prácticas y mucho más. Apúntate al estreno y consigue un descuento exclusivo 👇🏻.


Te recuerdo que puedes seguirme en mis redes sociales en Aristi.Dev. Y si tienes dudas con este o cualquier otro artículo del blog únete al Discord de la comunidad y te ayudaremos.