Last Updated: 2021-10-26

¿Qué es un API?

Una Interfaz de Programación de Aplicaciones (API, por sus siglas en inglés), es una interfaz que ofrece diversos servicios estandarizados los cuales permiten la integración entre varios componentes de software.

Estas funcionan a modo de caja negra, ya que eliminan la preocupación de que otros componentes deban conocer la implementación del software que expone los servicios. Un ejemplo son las API REST, las cuales usualmente existen como el medio de comunicación entre las interfaces gráficas de usuario (GUI) y los componentes de lógica del negocio (backend) usando el protocolo HTTP. En general, las API pueden ser utilizadas para cualquier módulo de software que quiera ser descentralizado, mientras sea capaz de proveer la definición de precondiciones y estructuras de entrada, y una serie de postcondiciones, o posibles respuestas.

¿Qué construirá?

Al final de este tutorial usted tendrá:

¿Qué aprenderá?

Al desarrollar este tutorial aprenderá:

¿Qué necesita?

Para cumplir con la tarea de consultar recursos disponibles en internet, Android ofrece una serie de herramientas y librerías de acceso abierto, entre las cuales destacan por popularidad Volley y Retrofit. En este tutorial, usted aprenderá a utilizar estas dos librerías y también aprenderá acerca de los conceptos y aspectos a tener en cuenta al momento de elegir una librería para hacer solicitudes de red.

Volley

Esta librería, desarrollada por Google, se caracteriza porque sus operaciones funcionan a modo de llamado a procedimiento remoto o RPC, y las respuestas son manejadas y almacenadas por completo en memoria caché, motivo por el cual, a pesar de ser compatible con múltiples tipos de respuesta, no es recomendable para operaciones de descarga grandes. Volley elimina la necesidad de escribir grandes extensiones de código relacionado a las solicitudes de red, ya que estandariza el manejo de las mismas, sin dejar de lado la personalización de las peticiones. El sitio oficial se encuentra en el siguiente enlace: https://developer.android.com/training/volley?hl=es-419.

Retrofit

La librería Retrofit es un proyecto de Square Open Source, una compañía de código abierto que desarrolla múltiples librerías de uso común para varios lenguajes de programación. Esta librería permite crear interfaces de Java/Kotlin para representar el API REST, donde las peticiones HTTP deben ser declaradas de forma explícita con anotaciones que permiten describir el método de la petición, los parámetros o el cuerpo, los encabezados, entre otros. La compatibilidad de Retrofit con múltiples tipos de respuesta depende del uso de serializadores, los cuales implican la construcción de objetos en el lenguaje de programación donde se declaren los atributos del cuerpo de la petición. Esta librería se basa en OkHttp. El sitio oficial se encuentra en el siguiente enlace: https://square.github.io/retrofit/.

Otras opciones

Es posible realizar invocaciones a APIs REST desde Android haciendo uso de otras librerías y herramientas como, por ejemplo, las siguientes:

Contexto de la aplicación

La interfaz que se utilizará en este proyecto fue creada para una aplicación de ejemplo de un curso de MISO. Dicha aplicación busca proveer una plataforma para que coleccionistas aficionados de discos de vinilo puedan intercambiar y comprar discos de este tipo, teniendo en cuenta todas las categorizaciones de los álbumes por nombre, género, entre otros. Si desea conocer todo el contexto y la documentación del proyecto del cual se toma el API REST, consulte el siguiente enlace: https://misw-4104-web.github.io/GuiasProyecto/generalidades.html#documentaci%C3%B3n-del-api. Este API REST se encuentra desplegado en una instancia de Heroku, a la cual se puede acceder desde un cliente HTTP que realice peticiones a la siguiente URL: https://public-back-sandbox-vinyls.herokuapp.com/.


Nota: note que dicha URL expone un API REST con ciertas rutas definidas, por lo cual no puede acceder a ella en su ruta GET / desde un navegador.

Métodos de interés

Para el caso de este tutorial se utilizarán únicamente los métodos relacionados con los coleccionistas por decisión arbitraria, ya que el enfoque de este tutorial se limita únicamente a explorar las librerías de consulta de datos por internet. Los siguientes son los endpoints que se consultarán desde la aplicación:

Para el caso de este tutorial, usted creará una aplicación nueva, la cual tendrá dos actividades con el mismo contenido de GUI, con la diferencia de que una de ellas implementará las invocaciones a APIs utilizando la librería Volley, mientras que la otra utilizará la librería Retrofit para este propósito.

Crear el proyecto

Abra Android Studio para crear un nuevo proyecto vacío en su sistema de archivos. Si no tiene un proyecto previamente abierto, verá un menú como el de la imagen 1; en este caso, presione la opción "Start a new Android Studio project". En caso de tener un proyecto abierto, debe buscar la opción "New Project..." del menú de la parte superior del IDE en la pestaña "File", la cual se muestra en la imagen 2 a continuación:

Al abrir Android Studio sin un proyecto se muestra un menú con varios botones para abrir o importar un proyecto, perfilar un APK o abrir configuraciones del IDE. La opción a elegir es la primera de arriba a abajo.

Imagen 1. Menú de inicio de Android Studio

Menú de paneles de la parte superior del IDE de Android Studio con la pestaña File (primera de izquierda a derecha) abierta y la opción \

Imagen 2. Menú superior de Android Studio

En ambos casos, posteriormente verá un panel, como el de la imagen 3, para seleccionar una plantilla para su proyecto. Seleccione la plantilla denominada "Empty Activity", luego presione el botón next para llenar un formulario con el nombre de la aplicación, el nombre del package, la ubicación en el sistema local de archivos, el lenguaje de programación y los Software Development Kit (SDK) soportados. Ingrese el nombre "API_Libs" para crear el proyecto y el paquete, y deje los demás parámetros con sus valores por defecto.

Panel con varias opciones de plantillas organizadas en filas de 4 elementos. La 4ta opción de izquierda a derecha es Empty activity

Imagen 3. Panel de selección de plantillas para el nuevo proyecto

Note que Android Studio genera una estructura de archivos con: los archivos de Gradle para las dependencias; los recursos XML para agregar multimedia y plantillas de componentes (en el directorio res); el AndroidManifest para declarar la estructura de la aplicación y sus componentes; y cualquier otro archivo de código fuente relacionado a las activities elegidas como plantilla (en el directorio src/java).

Ahora mismo, la interfaz de la aplicación creada no cuenta con elementos que permitan una interacción con las acciones del usuario en la pantalla. Usted va a modificar la aplicación para que tenga una distribución de Layout más fácil de manejar, la cual se puede agrupar en tres grupos, cada uno comprendido por un botón para efectuar el llamado de tres funciones al API REST y por un elemento de texto para mostrar el resultado de cada invocación. Además, en los dos últimos grupos habrá campos de texto para ingresar información requerida por la función del API REST.

En este caso, se optará por utilizar una organización a partir de LinearLayout, dado que este funciona mejor que el ConstraintLayout incluido por defecto en el proyecto, pues no requiere definir medidas relativas que pueden afectar el resultado de la interfaz según el tipo de pantalla en que sea renderizado.

En primer lugar, elimine el código que encuentra en el archivo activity_main.xml y reemplácelo con las siguientes líneas:

<?xml version="1.0" encoding="utf-8"?>

<ScrollView android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:orientation="vertical"
        tools:context=".MainActivity">
...
    </LinearLayout>
</ScrollView>

El espacio con los tres puntos suspensivos será donde ubique los elementos gráficos posteriormente. Para tener una referencia, la aplicación que se desea desarrollar se debe ver como en la siguiente imagen, que contiene una interfaz con un texto y un botón para consultar los coleccionistas existentes, seguidos de un texto, tres campos de entrada y un botón para crear un coleccionista, y finalmente un texto, cuatro campos de entrada y el botón que permite editar la información de un coleccionista indicado:

contiene un texto y un botón (Obtener coleccionistas) para consultar los coleccionistas existentes, seguidos de un texto, tres campos de entrada y un botón  (crear coleccionista) para crear un coleccionista, y finalmente un texto, cuatro campos de entrada y el botón (modificar coleccionista) que permite editar la información de un coleccionista indicado

Imagen 4. Vista en ejecución de la aplicación a desarrollar en este paso

En el esqueleto de código que descargó inicialmente, había un elemento TextView con el texto "Hello World!" escrito directamente en el código como un valor literal. Es importante que en sus aplicaciones de Android separe las cadenas de texto en el archivo strings.xml del directorio values en los recursos res por motivos de mantenibilidad del código. Abra dicho archivo e ingrese los textos que utilizará la aplicación en el botón y el espacio superior. Su archivo debe contener el siguiente código:

<resources>
    <string name="app_name">API_Libs</string>
    <string name="default_text">Haga clic en el botón para cargar información</string>
    <string name="post_text">Haga clic en el botón para crear un dato</string>
    <string name="put_text">Haga clic en el botón para modificar un dato</string>
    <string name="fetch_label">Obtener coleccionistas</string>
    <string name="post_label">Crear coleccionista</string>
    <string name="put_label">Modificar coleccionista</string>
    <string name="id_hint">ID</string>
    <string name="name_hint">John Doe</string>
    <string name="phone_hint">3112223344</string>
    <string name="mail_hint">example@domain.com</string>    
    <string name="action_switch_layout">Cambiar actividad</string>
</resources>

Estos recursos de texto pueden ser luego referidos desde la aplicación con el manejador de recursos R, el cual es una clase de Android que abstrae todo el contenido creado en el directorio resources del proyecto.

Ahora, modifique nuevamente el archivo activity_main.xml para incluir los elementos gráficos correspondientes haciendo uso de las cadenas de texto que acaba de definir. Los elementos gráficos mostrados en la imagen 4 son varios TextView, como el que venía por defecto, varios botones para invocar el API REST y varios campos de texto para los parámetros requeridos, los cuales en código se ven de la siguiente forma:

        <TextView
            android:id="@+id/get_result_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/default_text"
            android:textSize="30sp" />

        <Button
            android:id="@+id/fetch_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/fetch_label" />

        <View
            android:id="@+id/divider"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="?android:attr/listDivider" />

        <TextView
            android:id="@+id/post_result_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/post_text"
            android:textSize="30sp" />

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

             <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/txt_post_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/name_hint" />

        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/txt_post_phone"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/phone_hint" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/txt_post_mail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/mail_hint" />
        </com.google.android.material.textfield.TextInputLayout>

        <Button
            android:id="@+id/post_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/post_label" />

        <View
            android:id="@+id/divider2"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="?android:attr/listDivider" />

        <TextView
            android:id="@+id/put_result_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/put_text"
            android:textSize="30sp" />

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/txt_put_id"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/id_hint" />

        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/txt_put_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/name_hint" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/txt_put_phone"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/phone_hint" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/txt_put_mail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/mail_hint" />

        </com.google.android.material.textfield.TextInputLayout>

        <Button
            android:id="@+id/put_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/put_label" />

Ubique este mismo código en el espacio de los tres puntos suspensivos que debe haber en su archivo. Con esto verá que se modificó visualmente el layout que está definiendo para MainActivity. Si detalla los atributos de cada etiqueta, verá que se define un identificador para cada una, que servirá para referirse al elemento desde el código, y además, se define un tamaño y un posicionamiento, junto con el texto que define al elemento, haciendo referencia a los elementos que comprenden su archivo strings.xml.

Luego de crear el contenido para la actividad principal, donde utilizará Volley para invocar la API, usted debe crear una nueva actividad vacía, donde la invocación se hará por medio de Retrofit. Para esto, seleccione el directorio donde se ubica el código fuente del proyecto y haga clic en la opción New > Activity > Empty Activity. Esto le mostrará una ventana de configuración donde debe modificar el valor del campo "Activity Name" por "RetrofitActivity". Asegúrese de seleccionar la opción de crear un archivo de recursos para la interfaz gráfica y proceda a crear la nueva actividad con el botón "Finish".

Con lo anterior ahora tendrá un archivo activity_retrofit.xml en el directorio res > layout para representar la estructura de la interfaz gráfica de su nueva actividad. Copie el contenido del archivo activity_main.xml y reemplace con él el contenido actual de su archivo, creado automáticamente. Asegúrese de modificar todos los identificadores en el atributo android:id de cada etiqueta XML, agregando al final de todos los identificadores unos caracteres que ayuden a diferenciar los ids, como los caracteres "_2", de forma que, por ejemplo, el identificador "get_result_text" se vería como "get_result_text_2".

Por lo pronto su proyecto solo tendrá configurada la estructura de los elementos gráficos de la interfaz. La funcionalidad de la aplicación se define en pasos posteriores.

Incluir las dependencias necesarias

Por defecto, los proyectos creados en Android Studio no incluyen librerías ni dependencias. Para realizar peticiones en la red, es necesario que incluya, de forma manual, la librería correspondiente en los archivos de Gradle.

En el caso de Volley, las dependencias se agregan modificando el archivo build.gradle a nivel de app para que incluya la siguiente línea:

dependencies {
        ...
        implementation 'com.android.volley:volley:1.1.1'
    }

En este mismo apartado dependencies, incluya las dependencias necesarias para poder utilizar Retrofit, con las siguientes líneas:

implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.google.code.gson:gson:2.8.2'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'

Cuando termine de modificar los archivos, sincronice el proyecto con los nuevos cambios de Gradle con el botón Gradle Sync (ubicado en la parte superior derecha de Android Studio). Luego de que termine el proceso de sincronización, revise si el resultado es adecuado o si hubo algún error. Es posible que existan errores relacionados con su versión de Gradle o la versión de alguna de las librerías. Si este es el caso, Android Studio le ayudará a actualizarlas a las versiones adecuadas por medio de sugerencias del Lint (la herramienta de análisis estático de código de Android Studio). Para las aplicaciones que utilizan AndroidX, como en el caso de este tutorial, es posible que deba incluir otros pasos en la construcción del proyecto. En este caso, oriéntese por las instrucciones del siguiente enlace: https://developer.android.com/training/testing/set-up-project.

Agregar el permiso de internet

Para que una aplicación de Android pueda hacer peticiones en la red, es necesario que declare explícitamente que la aplicación solicita el permiso de enviar y recibir tráfico de internet, por medio de una etiqueta XML en el archivo de manifiesto. Este permiso no corresponde al tiempo de ejecución, sino que solo requiere ser declarado como una característica de la aplicación para funcionar. Si desea consultar más acerca del flujo de trabajo de Android para utilizar permisos, ingrese al siguiente enlace de Android Developers: https://developer.android.com/guide/topics/permissions/overview?hl=es_419.

Así, lo único que necesita para tener acceso a internet en su aplicación es incluir la siguiente línea en el archivo de manifiesto (AndroidManifest.xml), justo encima de la etiqueta <application>:

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

Ahora que fueron configurados los aspectos que permiten a la aplicación interactuar con APIs REST en internet, hay que realizar una serie de cambios en la aplicación para lograr la funcionalidad buscada.

En primer lugar, utilizaremos la actividad principal para hacer una implementación de las funciones de red que se requieren en la aplicación con la librería Volley. Posteriormente se replicará la vista en otra actividad, en la cual se implementarán las funciones de red con la librería Retrofit.

Para lograr esto, usted va a configurar la actividad MainActivity para vincular los botones de la interfaz con la invocación de las diferentes funciones del API REST haciendo uso de la librería Volley. Para esto, en el directorio del código fuente, cree un nuevo directorio llamado brokers, haciendo clic en el directorio con el nombre de paquete de Android y seleccionando la opción New > Package. Luego seleccione el directorio recién creado en el panel lateral, haga clic derecho y seleccione la opción New > Kotlin File/Class. En el diálogo que se despliega, seleccione la opción Class, ingrese el nombre "VolleyBroker" y confirme su selección con el botón Enter. En esta clase se va a declarar un único componente que maneja las peticiones al API haciendo uso del patrón Broker.

En este archivo usted declarará tanto la RequestQueue como las funciones que permiten definir las peticiones o requests que utilizará en este tutorial. Por este motivo, el constructor de esta clase debe pedir un contexto para declarar la cola de peticiones. El contenido de este archivo será el siguiente:

import android.content.Context
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import org.json.JSONObject

class VolleyBroker constructor(context: Context) {
    val instance: RequestQueue = Volley.newRequestQueue(context.applicationContext)

    companion object{
        const val BASE_URL= "https://vinyl-miso.herokuapp.com/"
        fun getRequest(path:String, responseListener: Response.Listener<String>, errorListener: Response.ErrorListener): StringRequest {
            return StringRequest(Request.Method.GET, BASE_URL+path, responseListener,errorListener)
        }
        fun postRequest(path: String, body: JSONObject,  responseListener: Response.Listener<JSONObject>, errorListener: Response.ErrorListener ):JsonObjectRequest{
            return  JsonObjectRequest(Request.Method.POST, BASE_URL+path, body, responseListener, errorListener)
        }
        fun putRequest(path: String, body: JSONObject,  responseListener: Response.Listener<JSONObject>, errorListener: Response.ErrorListener ):JsonObjectRequest{
            return  JsonObjectRequest(Request.Method.PUT, BASE_URL+path, body, responseListener, errorListener)
        }
    }

}

Podrá ver que los tres métodos funcionan como wrappers para crear una petición HTTP con el método correspondiente, sus parámetros y sus callbacks de respuesta y error. En el caso del método GET podrá ver un StringRequest, ya que la respuesta puede ser completamente procesada por medio de strings, mientras que los métodos POST y PUT deben ser JsonObjectRequest para poder procesar la petición y su respuesta como objetos complejos con el formato JSON.

Luego de esto, usted debe asignar la funcionalidad de los botones de la interfaz agregando los listeners correspondientes en el método OnCreate() de la actividad. También es necesario que declare un atributo que refiera a la instancia del broker desde su actividad para hacer uso de una misma cola de peticiones de Volley en todas las invocaciones al API REST. El contenido de su archivo MainActivity.kt debe ser el siguiente:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import com.example.api_libs.brokers.VolleyBroker
import com.google.android.material.textfield.TextInputEditText
import org.json.JSONObject

class MainActivity : AppCompatActivity() {
    lateinit var volleyBroker: VolleyBroker
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        volleyBroker = VolleyBroker(this.applicationContext)

        val getButton: Button = findViewById(R.id.fetch_button)
        val getResultTextView : TextView = findViewById(R.id.get_result_text)
        getButton.setOnClickListener {
            volleyBroker.instance.add(VolleyBroker.getRequest("collectors",
                Response.Listener<String> { response ->
                    // Display the first 500 characters of the response string.
                    getResultTextView.text = "Response is: ${response.substring(0, 500)}"
                },
                Response.ErrorListener {
                    Log.d("TAG", it.toString())
                    getResultTextView.text = "That didn't work!"
                }
                ))
        }

        val postButton: Button = findViewById(R.id.post_button)
        val postResultTextView : TextView = findViewById(R.id.post_result_text)
        postButton.setOnClickListener {
            val mailTxt : TextInputEditText = findViewById(R.id.txt_post_mail)
            val nameTxt : TextInputEditText = findViewById(R.id.txt_post_name)
            val phoneTxt : TextInputEditText = findViewById(R.id.txt_post_phone)
            val postParams = mapOf<String, Any>(
                    "name" to nameTxt.text.toString(),
                    "telephone" to phoneTxt.text.toString(),
                    "email" to mailTxt.text.toString()
            )
            volleyBroker.instance.add(VolleyBroker.postRequest("collectors", JSONObject(postParams),
                Response.Listener<JSONObject> { response ->
                    // Display the first 500 characters of the response string.
                    postResultTextView.text = "Response is: ${response.toString()}"
                },
                Response.ErrorListener {
                    Log.d("TAG", it.toString())
                    postResultTextView.text = "That didn't work!"
                }
            ))
        }

        val putButton: Button = findViewById(R.id.put_button)
        val putResultTextView : TextView = findViewById(R.id.put_result_text)
        putButton.setOnClickListener {
            val idTxt : TextInputEditText = findViewById(R.id.txt_put_id)
            val mailTxt : TextInputEditText = findViewById(R.id.txt_put_mail)
            val nameTxt : TextInputEditText = findViewById(R.id.txt_put_name)
            val phoneTxt : TextInputEditText = findViewById(R.id.txt_put_phone)
            val putParams = mapOf<String, Any>(
                    "id" to idTxt.text.toString(),
                    "name" to nameTxt.text.toString(),
                    "telephone" to phoneTxt.text.toString(),
                    "email" to mailTxt.text.toString()
            )
            volleyBroker.instance.add(VolleyBroker.putRequest("collectors", JSONObject(putParams),
                Response.Listener<JSONObject> { response ->
                    // Display the first 500 characters of the response string.
                    putResultTextView.text = "Response is: ${response.toString()}"
                },
                Response.ErrorListener {
                    Log.d("TAG", it.toString())
                    putResultTextView.text = "That didn't work!"
                }
            ))
        }
    }
}

Como se mencionó al inicio del paso anterior, ahora usted implementará una segunda actividad que funcione de la misma forma, pero que maneja las peticiones al API REST con la librería Retrofit. Recuerde que en un paso anterior usted agregó una actividad con el nombre RetrofitActivity. Este será el punto desde el cual se controle la interfaz gráfica y la funcionalidad de la segunda pantalla. No obstante, también se implementará un broker y, dadas las características particulares de Retrofit, una interfaz representando al API REST.

En primer lugar, cree un nuevo archivo llamado RetrofitApi en el directorio brokers del proyecto. En este archivo va a configurar la instancia de Retrofit y los servicios específicos que en ella se consultan y va a exponer un objeto con dicha instancia para ser utilizado desde otros lugares del proyecto. El contenido de dicho archivo debe ser el siguiente:

import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.http.*

private const val BASE_URL =
        "https://vinyl-miso.herokuapp.com/"

private val retrofit = Retrofit.Builder()
        .addConverterFactory(ScalarsConverterFactory.create())
        .baseUrl(BASE_URL)
        .build()

interface RetrofitApiService {
    @GET("collectors")
    fun getProperties():
            Call<String>

    @FormUrlEncoded
    @POST("collectors")
    fun postProperties(@Field("name") name: String,
                       @Field("telephone") telephone: String,
                       @Field("email") email: String):
            Call<String>
    
    @FormUrlEncoded
    @PUT("collectors")
    fun putProperties(@Field("id") id: String,
                      @Field("name") name: String,
                      @Field("telephone") telephone: String,
                      @Field("email") email: String):
            Call<String>
}

object RetrofitApi {
    val retrofitService : RetrofitApiService by lazy {
        retrofit.create(RetrofitApiService::class.java) }
}

Podrá notar que se utiliza un constructor de Retrofit con un ScalarsConverterFactory y la URL base. El convertidor le permite a Retrofit determinar lo que debe hacer con los datos que recibe en respuesta del servicio que se está consultando y, por ejemplo, determinar que las respuestas en formato JSON sean procesadas como cadenas de texto o bien, que sean mapeadas a un objeto. Además, podrá ver una interfaz con tres funciones que representan cada una de las invocaciones al API REST que deben hacerse con los botones. Estas funciones utilizan varias anotaciones de la librería de Retrofit, por ejemplo, para indicar el método HTTP y la ruta del endpoint que se va a invocar, además de los parámetros y sus correspondientes llaves en el JSON del cuerpo de la petición. Finalmente, podrá ver un objeto expuesto a manera de Singleton que crea el servicio de Retrofit a partir de la interfaz recién definida de forma lazy, lo que quiere decir que el objeto no se creará hasta que sea requerido por la aplicación.

Esta API ya puede ser utilizada desde cualquier parte del proyecto, pero hace falta definir el broker que se encargará de hacer los llamados a las funciones de red y retornar una respuesta que luego será mostrada en la actividad. Para esto, cree un nuevo archivo llamado RetrofitBroker, de la misma forma que hizo con el broker en el paso anterior. El contenido de este archivo debe ser el siguiente:

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class RetrofitBroker {
    companion object{
        fun getRequest(onResponse:(resp:String)->Unit, onFailure:(resp:String)->Unit) {
            var r = RetrofitApi.retrofitService.getProperties()
            var p = r.enqueue(
                    object : Callback<String> {
                        override fun onFailure(call: Call<String>, t: Throwable) {
                            onFailure(t.message!!)
                        }

                        override fun onResponse(call: Call<String>, response: Response<String>) {
                            onResponse(response.body()!!)
                        }
                    })
        }
        fun postRequest(body: Map<String, String>, onResponse:(resp:String)->Unit, onFailure:(resp:String)->Unit) : String? {
            var resp: String? = null

            RetrofitApi.retrofitService.postProperties(body["name"] ?: "", body["telephone"] ?: "", body["email"] ?:"").enqueue(
                    object : Callback<String> {
                        override fun onFailure(call: Call<String>, t: Throwable) {
                            onFailure(t.message!!)
                        }

                        override fun onResponse(call: Call<String>, response: Response<String>) {
                            onResponse(response.body()!!)
                        }
                    })
            return resp
        }
        fun putRequest(body: Map<String, String>, onResponse:(resp:String)->Unit, onFailure:(resp:String)->Unit) : String? {
            var resp: String? = null
            RetrofitApi.retrofitService.putProperties(body[""] ?:"", body["name"] ?: "", body["telephone"] ?: "", body["email"] ?:"").enqueue(
                    object : Callback<String> {
                        override fun onFailure(call: Call<String>, t: Throwable) {
                            onFailure(t.message!!)
                        }

                        override fun onResponse(call: Call<String>, response: Response<String>) {
                            onResponse(response.body()!!)
                        }
                    })
            return resp
        }
    }
}

Notará que los tres métodos funcionan como wrappers de una forma muy similar, para invocar las funciones del servicio del API de Retrofit y encolarles, definiendo un callback para cuando se recibe la respuesta y un callback para cuando se presenta un error. Ahora, lo único que falta es vincular estos métodos del broker a los respectivos botones de la interfaz. Para esto, abra el archivo RetrofitActivity y asegúrese que el código del método OnCreate() se vea de la siguiente forma:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_retrofit)

        val getButton: Button = findViewById(R.id.fetch_button_2)
        val getResultTextView : TextView = findViewById(R.id.get_result_text_2)
        getButton.setOnClickListener {
            RetrofitBroker.getRequest(onResponse = {
                getResultTextView.text = it
            }, onFailure = {
                getResultTextView.text = it
            })
        }

        val postButton: Button = findViewById(R.id.post_button_2)
        val postResultTextView : TextView = findViewById(R.id.post_result_text_2)
        postButton.setOnClickListener {
            val mailTxt : TextInputEditText = findViewById(R.id.txt_post_mail_2)
            val nameTxt : TextInputEditText = findViewById(R.id.txt_post_name_2)
            val phoneTxt : TextInputEditText = findViewById(R.id.txt_post_phone_2)
            val postParams = mapOf<String, String>(
                    "name" to nameTxt.text.toString(),
                    "telephone" to phoneTxt.text.toString(),
                    "email" to mailTxt.text.toString()
            )
            RetrofitBroker.postRequest(postParams,
                onResponse = {
                    postResultTextView.text = it
                }, onFailure = {
                    postResultTextView.text = it
                })
        }

        val putButton: Button = findViewById(R.id.put_button_2)
        val putResultTextView : TextView = findViewById(R.id.put_result_text_2)
        putButton.setOnClickListener {
            val idTxt : TextInputEditText = findViewById(R.id.txt_put_id_2)
            val mailTxt : TextInputEditText = findViewById(R.id.txt_put_mail_2)
            val nameTxt : TextInputEditText = findViewById(R.id.txt_put_name_2)
            val phoneTxt : TextInputEditText = findViewById(R.id.txt_put_phone_2)
            val putParams = mapOf<String, String>(
                    "id" to idTxt.text.toString(),
                    "name" to nameTxt.text.toString(),
                    "telephone" to phoneTxt.text.toString(),
                    "email" to mailTxt.text.toString()
            )
            RetrofitBroker.putRequest(putParams,
                    onResponse = {
                        putResultTextView.text = it
                    }, onFailure = {
                        putResultTextView.text = it
                    })
        }
    }


Asegúrese de importar cada librería que se utiliza allí. Al copiar el código, el IDE le avisará que existen dependencias que no se han importado, y le asistirá en su importación.

Agregar el botón de transición entre actividades

Para poder acceder a esta segunda actividad, es necesario definir un mecanismo para que el usuario pueda cambiar libremente entre las dos actividades del proyecto. Esto se logra con un botón en la barra de navegación de Android, en el ActionBar de la parte superior de la pantalla, el cual envía un Intent para abrir la actividad que utiliza la otra librería.

Para lograr esto, lo primero que debe hacer es crear un recurso en la carpeta res para incluir la estructura de elementos de la barra de navegación. Haga clic derecho sobre dicho directorio, seleccione la opción New > Android resource directory, esto abrirá un cuadro de diálogo, en el cual debe seleccionar "menu" como valor del campo resource type y confirmar con el botón OK. Esto crea un directorio para los archivos XML que definirán la estructura de los elementos de su ActionBar. Abra el archivo layout_menu.xml e incluya dentro del elemento <menu> un ítem representado con las siguientes líneas de código:

<item android:id="@+id/action_switch_layout"
   android:title="@string/action_switch_layout"
   app:showAsAction="always" />

Note que en las propiedades de este ítem se incluye un título, tomado de su archivo strings.xml ya definido. También se va a incluir un ícono, que será tomado del directorio res/drawable. Para agregar este ícono haga clic derecho sobre el directorio res/drawable y seleccione la opción New > Vector asset. Esto abre un diálogo para configurar el nuevo ícono, donde usted debe configurar un asset de tipo clip art cuyo ícono sea el de nombre "sync_alt", de color blanco. Asegúrese que el nombre del archivo esté correctamente referenciado desde la propiedad android:icon de su ítem.

Finalmente, debe hacer que en ambas actividades se infle el menú como parte del layout y asignarle la funcionalidad al botón del ActionBar. Para esto, debe sobreescribir dos métodos del ciclo de vida de la actividad que están relacionados a la barra de menú, los cuales son onCreateOptionsMenu y onOptionsItemSelected. En el caso del archivo MainActivity, el código relacionado a estos métodos debe verse de la siguiente forma:

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
   menuInflater.inflate(R.menu.layout_menu, menu)
   supportActionBar!!.title = "Volley"
   return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           // Create an intent with a destination of the other Activity
           val intent = Intent(this, RetrofitActivity::class.java)
           startActivity(intent)
           return true
       }
       else -> super.onOptionsItemSelected(item)
   }
}

Luego de hacer estos cambios e importar los elementos no definidos hasta el momento, copie el mismo código en el archivo RetrofitActivity, y cambie el valor del título en el método onCreateOptionsMenu por "Retrofit" para poder distinguir la actividad ACTUAL desde la interfaz de la aplicación. Además, cambie el segundo parámetro del Intent en el método onOptionsItemSelected por MainActivity::class.java para que el botón permita saltar de vuelta a la actividad principal si se encuentra en la actividad de Retrofit.

Ahora que tiene la aplicación implementada y la puede ejecutar, debe probar que ambas librerías sean consistentes con respecto al comportamiento declarado en la documentación del API REST, y que sepan interpretar todas las posibles respuestas del recurso que se está consumiendo. Para esto, usted debe probar diferentes escenarios que causan que la respuesta del servidor que está consultando varíe. Algunos de estos escenarios podrá llevarlos a cabo únicamente modificando el estado de su dispositivo, pero en algunos otros casos, debe realizar modificaciones en el código y recompilar la aplicación para ver reflejados los cambios. Siga los pasos indicados a continuación:

  1. Ejecute la aplicación con el código fuente desarrollado hasta el momento, podrá ver la aplicación tal como en la imagen 4 de este tutorial. Debe ver que el título de la aplicación es "Volley", lo cual indica que se encuentra en la actividad principal. Asegúrese que cuenta con conexión a internet y haga clic en el botón "Obtener coleccionistas". Podrá ver cómo luego de un tiempo la información del API se muestra en formato JSON como una larga cadena de texto. Luego, cambie de actividad con el botón de la parte superior derecha y repita el proceso. El resultado será el mismo.
  2. Repita el paso 1 pero ahora desactivando la conexión a internet de su celular. Podrá ver que en el caso de Retrofit, la excepción capturada es del tipo java.net.UnknownHostException. En el caso de Volley, la excepción capturada es del tipo com.android.volley.NoConnectionError, pero en el stack trace también se encuentra la misma excepción de la otra librería. Presione los tres botones de cada actividad y verá que el error es exactamente el mismo para los métodos GET, POST y PUT.
  3. Cree un nuevo coleccionista en cada una de las actividades. Podrá ver en la respuesta de cada librería que se retorna el identificador del coleccionista recién creado. Vuelva a obtener los coleccionistas y observe que al final de la cadena de texto se encuentra la información de los coleccionistas que usted creó para cada actividad.
  4. Modifique la información de un coleccionista con los identificadores que obtuvo, desde cada actividad. Podrá ver que los cambios se realizan efectivamente y también se pueden evidenciar desde cada actividad.
  5. Modifique la información de un coleccionista con un identificador que no existe. Podrá ver que en ambos casos se muestra un error del servidor que corresponde con el error indicado en la documentación de Postman.

¡Felicidades!

Al finalizar este tutorial, pudo familiarizarse con la realización de peticiones de red a interfaces API REST disponibles en internet por medio de las librerías Volley y Retrofit.

Ahora podrá incorporar a sus aplicaciones, la invocación de API REST remotas para obtener información, de forma que pueda desarrollar aplicaciones más dinámicas. Además, podrá explorar mejores formas de manejo de la información obtenida desde un API REST externa a su aplicación.

Créditos

Versión 1.1 - Octubre 25, 2021

Juan Sebastián Espitia Acero

Autor

Norma Rocio Héndez Puerto

Revisora

Mario Linares Vásquez

Revisor