AVISO Este capítulo ha sido actualizado con las últimas tecnologías en –> Capítulo 20 versión 2.0 <–

Si nos fijamos en la mayoría de aplicaciones que usamos habitualmente podemos apreciar que tienen un contenido dinámico, es decir, cambia cuando alguien añade información, fotos o lo que sea. Esto se debe a que la gran mayoría de aplicaciones acceden a la red para obtener los datos que posteriormente se muestran en la app. En este capítulo aprenderemos a entender cómo funciona y aplicarlo.

¿Qué es un API REST?

Un API REST es un servicio que nos provee de las funciones necesarias para poder obtener información de un cliente externo (una base de datos alojada en otra parte del mundo por ejemplo) dentro de nuestra propia aplicación.

Por ejemplo pensemos en Facebook, una aplicación con millones de usuarios. Sería inviable tener la información de cada usuario dentro de la aplicación ¿verdad? Pues para solventar el problema usan servicios API REST. Lo primero que hacemos al entrar en la app es un login, esto sería el primero de los servicios, pues nosotros le mandamos al servidor el usuario y contraseña y este nos devolvería la información que debemos mostrar en la app.

Tenemos cuatro tipos distintos de peticiones.

  • Get: Son las más sencillas, solo nos devuelven información. Si necesitamos pasarle un parámetro a la petición será a través de la url. Es decir si por ejemplo tenemos que hacer una petición que depende de una id (ej la identificación del usuario) la url se formaría así https://ejemplo.com/informacion/1, siendo 1 el parámetro que le pasamos. El problema de esto es que es poco seguro para pasar información delicada.
  • Post: Similar a Get pero los parámetros no se pasan por url, por lo que es más seguro para mandar información.
  • Put: Se suele usar para crear la entidad, es decir, si pensamos en un servicio como el acceso a una base de datos, este crearía el usuario por ejemplo.
  • Delete: Sería el último de los cuatro que nos permitiría borrar los registros de la base de datos.

La información suele venir en dos formatos distintos, XML o JSON. Para no meternos mucho en el tema, solo hablaremos del JSON que es el formato más habitual y con el que nosotros vamos a trabajar.

Formato JSON

Json es un formato de texto simple, es el acrónimo de JavaScript Object Notation. Se trata de uno de los estándar para el traspaso de información entre plataformas, tiene una forma muy legible que nos permite entender su contenido sin problema. Un ejemplo sencillo sería este.

{
  "employees": {
    "employee": [
      {
        "id": "1",
        "firstName": "Tom",
        "lastName": "Cruise",
        "photo": "https://jsonformatter.org/img/tom-cruise.jpg"
      },
      {
        "id": "2",
        "firstName": "Maria",
        "lastName": "Sharapova",
        "photo": "https://jsonformatter.org/img/Maria-Sharapova.jpg"
      },
      {
        "id": "3",
        "firstName": "Robert",
        "lastName": "Downey Jr.",
        "photo": "https://jsonformatter.org/img/Robert-Downey-Jr.jpg"
      }
    ]
  }
}

Todo formato Json empieza y termina con llaves y tiene una clave-valor. La clave employees contiene a su vez una lista de employee (fijaros que en vez de llaves tiene corchetes), que este almacena idfirstNamelastNamephoto. Así podemos pasarnos gran cantidad de información de una plataforma a otra con unos estándar que nos ayudan a simplificar el proceso.

Si aún así se os complica la lectura de estos ficheros al principio, podemos hacer uso de multitud de webs que simplifican la forma de verlo, como por ejemplo JsonEditOnline.

Curso programación android en kotlin

Otras formas de leer un Json

Empezamos

En este capítulo haremos una pequeña app que busca imágenes de perros. Tendremos un buscador y una lista. El usuario añadirá una raza de perro y en ese momento haremos una petición REST que nos devolverá una array de imágenes de dicha raza y  actualizaremos la lista.

Utilizaremos un API pública llamada Dog API que podemos encontrar aquí.

Se trata de un servicio GET, al cual le pasaremos el nombre de la raza y nos devolverá un Json similar a este.

{
  "status": "success",
  "message": [
    "https://images.dog.ceo/breeds/akita/512px-Ainu-Dog.jpg",
    "https://images.dog.ceo/breeds/akita/512px-Akita_inu.jpeg",
    "https://images.dog.ceo/breeds/akita/Akina_Inu_in_Riga_1.JPG",
    "https://images.dog.ceo/breeds/akita/Akita_Dog.jpg",
    "https://images.dog.ceo/breeds/akita/Akita_Inu_dog.jpg",
    "https://images.dog.ceo/breeds/akita/Akita_inu_blanc.jpg",
    "https://images.dog.ceo/breeds/akita/An_Akita_Inu_resting.jpg",
    "https://images.dog.ceo/breeds/akita/Japaneseakita.jpg"
  ]
}

Debemos fijarnos en que contiene dos atributos:

  • Status: Se trata de una String que nos devolverá successerror dependiendo de si encuentra la raza o no.
  • Message: Una array de String que contiene todas las imágenes que mostraremos.

Te preguntarás cómo podemos hacer la llamada y recuperar los valores, pues para ello usaremos dos librerías, Retrofit, que será la encargada de hacer las peticiones a red y Gson que será el encargado de parsear la respuesta, es decir, tratará el Json para convertirlo en un objeto de los que trabajamos en Kotlin.

Accediendo a los datos

Empezaremos añadiendo las librerías mencionadas anteriormente. Iremos al fichero build.gradle del módulo app para poder hacerlo.

ext.retrofit_version = '2.3.0'
ext.cardView_version = '27.1.0'
ext.support_version = '27.1.0'
ext.picasso_version = '2.71828'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'

    //Picasso
    implementation "com.squareup.picasso:picasso:$picasso_version"

    //Suport
    implementation "com.android.support:appcompat-v7:$support_version"
    implementation "com.android.support:recyclerview-v7:$support_version"

    //Retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

    //Anko
    implementation 'org.jetbrains.anko:anko-common:0.9'

    //Cardview
    implementation "com.android.support:cardview-v7:$cardView_version"


    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

Esta vez, he añadido las versiones de las librerías en variables usando la palabra reservada ext.NombreQueQueramos. Esto lo hago porque el código luego queda más legible y es más fácil de tocar a la hora de actualizar.

Fijémonos en las librerías implementadas:

  • Picasso: Lo usaremos para cargar las imágenes de internet, ya lo hemos usado anteriormente en el capítulo 15.
  • Retrofit: Como ya hemos mencionado anteriormente la usaremos para la petición REST.
  • GSon: Convertirá el Json en un modelo de datos fácil para poder trabajar con él.
  • Anko: Ya lo hemos usado varias veces, lo usaremos para simplificar la práctica lo máximo posible.
  • CardView: Es un componente de Android, que nos permite crear una especie de tarjetas que visualmente quedan muy bien, será el contenedor de nuestras imágenes.

El resto son librerías que vienen por defecto por lo que no entraré en ellas.

Seguidamente crearemos el modelo de datos, es decir una clase que permita a Gson extraer los valores del Json y crear un objeto de la clase con dichos valores.

Se tratará de una data class llamada DogsResponse y contendrá dos campos, los mismos que nos devolvía la respuesta de la petición.

data class DogsResponse (@SerializedName("status") var status:String, @SerializedName("message") var images: List<String>)

Como ven, es una clase muy habitual de las que hemos trabajado, la única diferencia es el atributo @SerializedName, que usaremos para añadir el nombre exacto del valor que devuelve el API. Este atributo lo podemos quitar si el nombre de nuestro campo es exacto al que devuelve la respuesta, pero recomiendo dejarlo.

En este ejemplo crearemos una sola petición, que será la lista de imágenes, pero podemos tener muchas más funciones con respuestas distintas, todas ellas irán en una interfaz (no una clase) que crearemos llamada APIService.

interface APIService {
    @GET
    fun getCharacterByName(@Url url:String): Call<DogsResponse>
}

Aunque sea muy cortita debemos entender ciertos aspectos. Antes de cada función debemos decir que tipo de llamada REST será, GET, POST, etc. Luego hemos añadido @URL al parámetro que recibirá, puesto que será el nombre de la raza y al ser GET irá en la url. Para terminar nos devolverá un objeto de la clase DogsResponse con toda la información. Fijaros que va dentro de un objeto Call<> de Retrofit.

Ahora iremos al MainActivity a añadir la implementación de retrofit.

Crearemos un método que nos devuelva un objeto Retrofit, que será el encargado de la petición.

private fun getRetrofit(): Retrofit {
    return Retrofit.Builder()
            .baseUrl("https://dog.ceo/api/breed/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
}

Para poder crearlo llamaremos al Builder() de la librería retrofit, en la cual hay que pasarle el baseUrl, que es la url donde haremos la petición, pero solo la base, es decir https://dog.ceo/api/breed/ (importante el / final o nos dará error).

La url de la petición completa será https://dog.ceo/api/breed/akita/images, pero como la parte final es dinámica (depende de la raza que queramos buscar) no lo añadimos al baseUrl.

Después, antes del build, añadimos la factoría que convierte el Json en nuestra clase DogsResponse, que es la línea addConverterFactory.

Debemos entender que una petición se hace de manera asíncrona es decir, nosotros empezamos la petición y como no sabemos lo que puede tardar, una vez tenga los datos nos avisará. Este proceso podría bloquear la aplicación, por que usamos los hilos. Los hilos nos permiten crear subprocesos dentro de la app sin bloquear el hilo principal, que es el que gestiona la interfaz. Por ello debemos crear un hilo que gestione la llamada. Más información sobre los subprocesos en la página oficial de desarrolladores de Android.

private fun searchByName(query: String) {
     doAsync {
         val call = getRetrofit().create(APIService::class.java).getCharacterByName("$query/images").execute()
         val puppies = call.body() as DogsResponse
         uiThread {
             if(puppies.status == "success") {
                 initCharacter(puppies)
             }else{
                 showErrorDialog()
             }
             hideKeyboard()
         }
     }
 }

La creación de un hilo es muy sencilla, gracias a Anko, simplemente llamaremos a la función doAsync{} y todo lo que hagamos dentro se gestionará en otro hilo.

Procedemos a entender que hemos hecho. Lo primero es crear una variable llamada call que se encargará de llamar a la función getRetrofit() que creamos previamente, seguido de la interfaz que contiene la llamada que queremos y terminamos pasando la query (que será la raza del perro que hemos puesto en el buscador) y llamando posteriormente a la función execute().

El contenido de esa variable será la respuesta de nuestra API, pero Retrofit nos devolverá un objeto genérico con más contenido del que buscamos, por lo que creamos una variable puppies que llamará a la variable anterior call seguido de .body() que permite extraer solo la información de nuestro interés. Seguidamente como es un objeto genérico le debemos decir que es del tipo DogsResponse.

Con esto ya tendríamos la información que nos interesa, lo siguiente será modificar la vista para añadir la nueva información, pero como he dicho anteriormente, la parte visual se trabaja en el hilo principal por lo que llamaremos dentro del doAsync{} a la función uiThread{} que nos permite ejecutar parte del código en el hilo principal.

Ahora comprobaremos si el estado es success, lo que significa que el API nos ha devuelto los valores correctos y si es así iniciaremos la vista o mostraremos un diálogo de error y para terminar, independientemente del resultado, ocultaremos el teclado. Esta parte todavía no la hemos desarrollado.

Diseño

El diseño será muy sencillo, nuestro activity_main contendrá un SearchView y un RecyclerView, ambos irán dentro de un ConstraintLayout.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    android:id="@+id/viewRoot"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.cursokotlin.retrofitkotlinexample.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvDogs"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/searchBreed"/>

    <android.support.v7.widget.SearchView
        android:id="@+id/searchBreed"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="7dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0"
        app:queryHint="Ej: Akita" />

</android.support.constraint.ConstraintLayout>

En el RecyclerView hemos dicho que coja el ancho total de la pantalla y la altura será igual pero empezando debajo del buscador. El SearchView tiene una propiedad nueva, queryHint que nos permite añadir una descripción para que el usuario vea un ejemplo de lo que debe buscar.

Como el SearchView no lo hemos visto, será lo primero que implementemos en la MainActivity. Para que funcione debemos hacer que nuestra activity implemente los métodos necesarios.

class MainActivity : AppCompatActivity(), android.support.v7.widget.SearchView.OnQueryTextListener {

También deberemos añadir en la función onCreate() su listener.

searchBreed.setOnQueryTextListener(this)

Una vez añadida la extensión nos pedirá que implementemos los métodos necesarios.

override fun onQueryTextChange(newText: String?): Boolean {
    return true
}

override fun onQueryTextSubmit(query: String): Boolean {
    return true
}

El primer método onQueryTextChange, nos avisará cada vez que el usuario añada un carácter, pero este no lo utilizaremos porque solo buscaremos la raza una vez el usuario termine de escribir. Nos centraremos en OnQueryTextSubmit, que se lanzará una vez el usuario pulse en la acción de que ha terminado de escribir, por ello nos devuelve una String, que será el texto introducido.

Aprovecharemos la función OnQueryTextSubmit para lanzar la petición una vez el usuario termina de escribir en el buscador, por lo que modificaremos la función para llamar a searchByName().

override fun onQueryTextSubmit(query: String): Boolean {
     searchByName(query.toLowerCase())
     return true
 }

Antes de mandar la búsqueda, hemos llamado a la función ToLowerCase() que transforma todo el texto en minúsculas.

Una vez lo mandemos y se realice la petición, deberemos mostrar las imágenes, para ello usaremos nuestro recyclerView, por lo que el siguiente paso será crear el adapter DogsAdapter y su vista, empezamos por esta última, a la cual llamaremos item_dog.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="320dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_margin="16dp"
    android:elevation="7dp"
    app:cardCornerRadius="8dp">

    <ImageView
        android:id="@+id/ivDog"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.v7.widget.CardView>

 Como habíamos dicho al principio del capítulo, estas vistas irán en un cardView. El único atributo nuevo será cardCornerRadius que será el nivel del borde redondeado, mientras más le pongamos, más cerrado serán las esquinas.

Nuestro adapter será muy sencillo.

class DogsAdapter (val images: List<String>) : RecyclerView.Adapter<DogsAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = images[position]
        holder.bind(item)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return ViewHolder(layoutInflater.inflate(R.layout.item_dog, parent, false))
    }

    override fun getItemCount(): Int {
        return images.size
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        fun bind(image: String) {
            itemView.ivDog.fromUrl(image)
        }
    }
}

En la función bind() del ViewHolder, hemos cogido la imagen de la vista y hemos llamado a una función de extensión que hemos creado llamada fromUrl(). Esta extensión la crearemos en un fichero nuevo (yo lo suelo llamar extensions ej ImageExtensions, DateExtensions).

fun ImageView.fromUrl(url:String){
    Picasso.get().load(url).into(this)
}

La extensión aprovecha la librería Picasso para convertir la url de la imagen en un elemento visual.

Con esto hemos terminado la parte del adapter y podemos volver al MainActivity.

Recordemos que una vez la petición nos responda podremos realizar dos opciones, la primera cargar los elementos obtenidos en el recyclerView.

 private fun initCharacter(puppies: DogsResponse) {
    if(puppies.status == "success"){
        imagesPuppies = puppies.images
    }
    dogsAdapter = DogsAdapter(imagesPuppies)
    rvDogs.setHasFixedSize(true)
    rvDogs.layoutManager = LinearLayoutManager(this)
    rvDogs.adapter = dogsAdapter
}

Primero nos aseguramos que el estado sea success por si cometemos el error de llamar al método desde otro lado, si es así, asignaremos el resultado de la respuesta en una array genérica que hemos creado en la parte superior de la clase.

lateinit var imagesPuppies:List<String>

Acto seguido iniciaremos el recyclerView con dicha lista.

Ahora nos quedarán dos métodos para acabar el proyecto. El primero mostrará un error si el servicio ha fallado.

lateinit var imagesPuppies:List<String>
private fun showErrorDialog() {
        alert("Ha ocurrido un error, inténtelo de nuevo.") {
            yesButton { }
        }.show()
    }

El segundo bajará el teclado una vez el usuario busque una raza.

private fun hideKeyboard(){
       val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
       imm.hideSoftInputFromWindow(viewRoot.windowToken, 0)
   }

Puedes ver la clase MainActivity completa aquí.

Con esto ya tendríamos todo. El último paso será ir al AndroidManifest y añadir permisos de internet para poder hacer la llamada.

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

El resultado final sería el siguiente.

Ejemplo app completa

Este capítulo es un poco más complicado que los anteriores, pero quería mostrarlo lo antes posible porque es imprescindible para cualquier aplicación. Cualquier dudas podéis dejar un comentario.

También puedes ver o descargar el proyecto completo desde mi Github.

Continúa con el curso: Capítulo 21 – Gestión de permisos en Android