Actualmente las empresas necesitan ofrecer a sus usuarios muchos terminales para usar sus servicios (web apps, aplicaciones móviles, redes sociales, etc.) y es común que debajo de esta multitud de interfaces exista una sola base de datos compartida.
Una forma de solucionar este problema es separando los datos de la presentación. Así tendríamos una arquitectura de varias capas en donde una base de datos MySQL, por ejemplo, puede estar alojada en un servidor en AWS y este sólo debe exponer una API a ser compartida entre:
- un aplicación Android alojada en la App Store,
- una web app alojada en Heroku,
- una aplicación de escritorio para macOS,
- etc.
Las APIs REST son interfaces para compartir recursos entre clientes y servidor. Mediante solicitudes HTTP podemos lograr que todos hablen el mismo idioma, sin importar el sistema operativo, plataforma o el lenguaje de programación de cada uno.
¿¡Quién es ese Pokémon!?
Ahora vamos a crear una pequeña app en Android Studio para demostrar a pequeña escala la diferencia entre datos y presentación. Como hemos mencionado dos partes arquitectónicamente bien definidas, para este ejemplo podemos enfocarnos sólo en el cliente, dejando de lado la creación de la base de datos. Es decir, vamos a consumir una API existente y disponible públicamente.
La PokeAPI es justamente eso: una API que contiene un índice de todos los Pokémon (a estas alturas, ya perdí la cuenta de cuántos son) junto con sus datos: altura, tipo, movimientos, etc.
Así que vamos a recrear la Pokédex: la enciclopedia de los Pokémon. La PokeAPI también contiene URLs de imágenes de cada Pokémon, así que cuando hagamos clic en uno y entremos a su vista detallada, aparecerá su foto.
Paso 1 – Crear un nuevo proyecto de Android Studio
Abre Android Studio y crea un nuevo proyecto. Selecciona la plantilla Master/Detail Flow, luego clic en Next y Finish.
Paso 2 – Configuración inicial
Abre el archivo build.gradle de tu aplicación y añade las siguientes dependencias:
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
}
Retrofit es un cliente REST extremadamente simple de configurar. Nos permitirá tratar las llamadas a la API como funciones Java, así definiremos solamente las URLs que queremos llamar y los tipos de petición y respuesta que esperamos.
Gson es una librería de Google muy popular para serializar y de-serializar data entre objetos JSON y objetos Java. Así, sin mucho boilerplate, podremos tratar a los datos JSON como clases de Java, pasando de esto:
{
"count": 1118,
"results": [
{
"name": "bulbasaur",
"url": "https://pokeapi.co/api/v2/pokemon/1/"
}
]
}
A esto:
public class PokemonFetchResults {
@SerializedName("count")
@Expose
private int count;
@SerializedName("results")
@Expose
private ArrayList results;
}
También hay que conceder permiso a la app para que se pueda conectar a Internet. Agrega la siguiente línea a tu AndroidManifest.xml:
uses-permission android:name="android.permission.INTERNET" /
Paso 3 – Definir endpoints y modelos
Crea una nueva Interfaz llamada PokemonAPIService:
import com.example.pokedex.resources.PokemonFetchResults;
import retrofit2.Call;
import retrofit2.http.GET;
public interface PokemonAPIService {
@GET("pokemon/?limit=50")
Call getPokemons();
}
Necesitaremos hacer 2 diferentes tipos de llamadas a la PokeApi:
- Una llamada GET a https://pokeapi.co/api/v2/pokemon?limit=50 para obtener los primeros 50 Pokémon
- Una llamada GET a https://pokeapi.co/api/v2/pokemon/{id} para obtener los detalles de cada uno.
También creamos dos nuevas clases: PokemonFetchResults y Pokemon:
PokemonFetchResults.java
public class PokemonFetchResults {
@SerializedName("results")
@Expose
private ArrayList results;
public ArrayList getResults() {
return results;
}
}
Pokemon.java
public class Pokemon {
@SerializedName("name")
@Expose
private String name;
@SerializedName("url")
@Expose
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return "It's " + getName() + "!";
}
}
Estos son los modelos correspondientes a los recursos que la PokeApi nos devuelve. Nota como no es necesario definir un campo si no lo necesitamos; por ejemplo, en PokemonFetchResults sólamente hemos declarado results, pese a que la API nos retorna otros campos como count, next y previous.
Paso 4 – Juntar todo en la vista principal
Ahora vamos a editar un archivo que viene con el template que seleccionamos. En la función onCreate de ItemListActivity añadimos lo siguiente:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://pokeapi.co/api/v2/")
.addConverterFactory(GsonConverterFactory.create(
new GsonBuilder().serializeNulls().create()
))
.build();
PokemonAPIService pokemonApiService = retrofit.create(PokemonAPIService.class);
Call call = pokemonApiService.getPokemons();
Primero hemos creado una nueva instancia de Retrofit y le proporcionamos una URL base. También le indicamos a la librería que utilice Gson para que transforme los resultados en objetos Java.
Debajo de estas líneas, añade lo siguiente:
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
if (response.isSuccessful()) {
ArrayList pokemonList = response.body().getResults();
View recyclerView = findViewById(R.id.item_list);
assert recyclerView != null;
setupRecyclerView((RecyclerView) recyclerView, pokemonList);
} else {
Log.d("Error", "Something happened");
return;
}
}
@Override
public void onFailure(Call call, Throwable t) {
Log.d("Error", t.toString());
}
});
Aquí hemos hecho una llamada asíncrona al endpoint que declaramos en getPokemons. Cuando dicha llamada se resuelva, se ejecutará la función Callback que extrae una lista de objetos Pokémon y termina de inicializar la vista principal. Notarás también que hemos cambiado la signatura de setupRecyclerView para que reciba un ArrayList de Pokemon en lugar de los datos dummy que vienen con la plantilla.
Necesitamos hacer dos cambios más. Primero a la función onBindViewHolder para que muestre el número y nombre de cada Pokémon:
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.mIdView.setText(Integer.toString(position));
holder.mContentView.setText(mValues.get(position).getName());
holder.itemView.setTag(position);
holder.itemView.setOnClickListener(mOnClickListener);
}
Y luego a SimpleItemRecyclerViewAdapter para que pase la información necesaria a la vista detallada:
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
int index = (int) view.getTag();
Pokemon item = mValues.get(index);
Context context = view.getContext();
Intent intent = new Intent(context, ItemDetailActivity.class);
intent.putExtra(ItemDetailFragment.ARG_ITEM_ID, index + 1);
intent.putExtra(ItemDetailFragment.ARG_ITEM_NAME, item.getName());
intent.putExtra(ItemDetailFragment.ARG_DESCRIPTION, item.getDescription());
context.startActivity(intent);
}
};
Como quizás lo sepas, los Pokémon están numerados desde el 1, y las URLs de la API respetan dicho orden (por ejemplo: https://pokeapi.co/api/v2/pokemon/6/ devuelve información del 6to Pokémon de la Pokédex: Charizard). En nuestra aplicación, ARG_ITEM_ID guarda ese valor. Usaremos toda esta información para hacer peticiones formadas dinámicamente según el ARG_ITEM_ID de cada vista detallada.
Paso 5 – Diseñar la UI para nuestra app de Android
Hasta ahora no habíamos tocado la UI porque casi todo lo que necesitamos ya está listo; pero es necesario hacer un cambio en el layout para poder mostrar la foto del Pokémon que el usuario seleccione.
Para ello agregaremos otra dependencia a build.gradle llamada Glide, que nos permite reemplazar “en caliente” una imagen mostrada en un componente ImageView.
dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0'
}
Ahora, en activity_item_detail, añadimos el ImageView, lo llamamos item_image y lo agregamos como nodo hijo de toolbar_layout.
activity_item_detail.xml
En la vista de código del mismo archivo, el ImageView tiene las siguientes propiedades:
ImageView
android:id="@+id/item_image"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_gravity="center_horizontal"
android:scaleType="center"
android:visibility="visible" /
Paso 6 – La vista detallada
Sólo nos queda una cosa por hacer: en ItemDetailFragment, en la función onCreate llamaremos a la API nuevamente para obtener la imagen del Pokémon seleccionado.
if (appBarLayout != null) {
appBarLayout.setTitle(activity.getIntent().getStringExtra(ARG_ITEM_NAME));
}
int pokemonNumber = activity.getIntent().getIntExtra(ARG_ITEM_ID, 0);
ImageView itemImage = activity.findViewById(R.id.item_image);
String pokemonImageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/" + pokemonNumber + ".png";
Glide.with(this)
.load(pokemonImageUrl)
.into(itemImage);
Notarás que en pokemonImageUrl la URL base es la misma; sólo tuvimos que insertar el pokemonNumber apropiado, que en realidad el valor de ARG_ITEM_ID.
En la función onCreateView, editamos la siguiente línea para que el contenido de la vista sea igual al valor de la descripción que pasamos desde la vista general:
((TextView) rootView.findViewById(R.id.item_detail)).setText(
getActivity().getIntent().getStringExtra(ARG_DESCRIPTION)
);
¡Eso es todo! Guarda tus cambios y corre el programa. Deberías obtener algo que se vea más o menos como lo que mostramos al inicio de este artículo.
Para concluir
En este tutorial hemos conectado a un cliente con una base de datos sin saber ningún detalle sobre esta (¿en dónde está alojada?, ¿es un servidor MySQL o MongoDB?, etc.) ya que la API abstrae dichos detalles y nos devuelve los recursos que solicitamos en formato JSON. En un próximo artículo veremos la otra cara de esta arquitectura, es decir, cómo configurar una base de datos y “conectarla” al mundo exterior por medio de una API.