# Consumo de APIs con Retrofit
Vamos a realizar una aplicación que consultará una API para ello usaremos:
- Retrofit: biblioteca de terceros para consultar APIs
- Coil: biblioteca creada por la comunidad para descargar, almacenar en búfer, decodificar y almacenar en caché tus imágenes.
Importante: vamos a suponer que todos han realizado las actividades de la Unidad 5 del curso de aspectos básicos del Compose
[UNIDAD 5 : Cómo conectarse a internet](http://developer.android.com/courses/android-basics-compose/unit-5?hl=es-419)
Lo primero es automatizar el manejo de dependcias con Hilt. Seguimos el siguiente tutorial para instalar la última versión:
[Inserción de dependencias con HILT](https://developer.android.com/training/dependency-injection/hilt-android?hl=es-419#kts)
Para incluir la librería retrofit en Module: app build.gradle.kts , en dependencias incluimos:
```
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
```
Y para COIL averiguarlo aquí:
https://github.com/coil-kt/coil#jetpack-compose
Usaremos la siguiente versión de la navegación que también hay que incluir en las dependencias de la app:
https://developer.android.com/guide/navigation/navigation-getting-started?hl=es-419
`
val nav_version = "2.5.3"
implementation("androidx.navigation:navigation-compose:$nav_version")`
El build del module debería quedar:
```
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
}
android {
namespace = "com.anluisa.gamesretrofit"
compileSdk = 34
defaultConfig {
applicationId = "com.anluisa.gamesretrofit"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
val nav_version = "2.6.0"
implementation("androidx.navigation:navigation-compose:$nav_version")
implementation("com.google.dagger:hilt-android:2.46.1")
kapt("com.google.dagger:hilt-android-compiler:2.46.1")
// Retrofit
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("io.coil-kt:coil-compose:2.5.0")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.1")
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
```
Y el del project:
> // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
id("com.google.dagger.hilt.android") version "2.46.1" apply false
}
## Dagger Hilt y estructura
Tal y como hicimos la vez anterior debemos crear una aplicación en la que indiquemos que esto va a ser manejado por Dagger Hilt.
Creamos una clase GameApplication que sólo dirá "ey...esto es una aplicación"
```
@HiltAndroidApp
class GameApplication: Application (){
}
```
Y en el manifest
```
<application
android:name=".GameApplication"
```
Además antes de la declaración del mainActivity debemos incluir:
```
@AndroidEntryPoint
```
Ya que estamos en el manifest aprovechamos y damos los permisos para acceder a internet. Añadimos al principio:
```
<uses-permission android:name="android.permission.INTERNET"/>
```
Creamos también el package "data" donde vamos a poner todo lo relativo a las consultas a la API, dentro crearemos una interface GameApi que dejamos vacía por ahora.

Creamos el package "di" para las dependencias y dentro el objeto AppModule.
La primera dependencia que incluimos es la del retrofit
```
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun providesRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
```
Ahora mismo da error porque en BASE_URL debemos proporcinar la URL de nuestra API, como eso será una constante en lugar de introducirla aquí creamos otro package "util" y la clase "Constants" donde incluiremos la URL de la API.
```
class Constants {
companion object{
const val BASE_URL=""
}
}
```
Nos faltaría la función para también proveer GameApi así que:
```
@Singleton
@Provides
fun providesGameApi(retrofit:Retrofit): GameApi{
return retrofit.create(GameApi::class.java)
}
```
Como vemos volvemos a tener una clase que va a comunicarse con la API y y otra que se va a encargar de mostrar los datos a nuestra UI para poder mantener la arquitectura limpia.
## API RAWG
Vamos a usar la API proporcionada por esta plataforma:
https://rawg.io/
Hay una parte gratuita y otra de pago, dónde podemos mirarlo? vamos al apartado API y vamos a tener que generar tanto una API key que nos identificará unívocamente (es posible que estén limitadas las funcionalidades, número de consultas, rapidez de la respuesta, etc dependiendo de lo que pague tu usuario).
https://rawg.io/apidocs
Vamos a la documentación y específicamente a games:

También nos dice que debemos incluir una APIKEY en cada consulta por lo que nos debemos registrar con alguna cuenta que no tenga datos personales para conseguir el API key, pondré el mío pero puede haber conflictos si lo usamos a la vez y ya ven que tiene búsquedas limitadas:
623db30bcbf14c5ab621f33d521fdc54

Vamos a observar el json y recordamos lo que vimos el año pasado de Json y xml, investiga un poco sobre la estructura del json y mira a ver si consigues suponer cuántos modelos de datos podríamos necesitar?
```
class Constants {
companion object{
const val BASE_URL="https://api.rawg.io/api/"
const val ENDPOINT="games"
const val API_KEY="?key=623db30bcbf14c5ab621f33d521fdc54"
const val CUSTOM_BLACK=0xFF2B2626
}
}
//Ejemplos de endpoints:
//Recuperamos todos los juegos
//https://api.rawg.io/api/games?key=623db30bcbf14c5ab621f33d521fdc54
//recuperamos un juego con un cierto ID
//https://api.rawg.io/api/games/1123?key=623db30bcbf14c5ab621f33d521fdc54
```
## Modelo e interface
Si vemos el json vemos que vamos a necesitar al menos dos modelos de datos, uno para recuperar el primer nivel (count del total de juegos, links para paginación, ...) y otro para recuperar los datos de cada juego que estarán dentro de results(id de cada juego, título, ):

Así que vamos a crear los modelos utilizando exactamente los nombres y los tipos de datos especificados en el json, no nos hacen falta todos los datos pero sí que los nombres y tipos sean exactos o dará error.
Así que creamos un package "model" y dentro un data class de nombre "GamesModel"
```
data class GamesModel(
val count: Int,
val results: List<GameList> //los resultados están formados de items que siguen el modelo GameList
)
//cada resultado recuperado
data class GameList(
val id: Int,
val name: String,
val background_image: String
)
```
Vamos a nuestra interface GamesApi y tal y como ya hicimos con el proyecto de Crono empezamos a generar las consultas. En lugar de los @Query del ROOM usaremos lo típico en APIs, el @GET.
Tengamos en cuenta también que estaremos consultando con internet así que debemos utilizar todo lo que vimos de los corrutinas y estado de nuestros procesos. Repasemos por si acaso:
https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=es-419
```
interface GameApi {
@GET(ENDPOINT + API_KEY) //formamos el endpoint llamándolos con el método get
suspend fun getGames(): Response<GamesModel> //cómo es una consulta tenemos que
}
```
Ya tenemos una función de la interface para obtener todos los juegos y sabemos que para poder mostrarlos necesitamos utilizar el patrón MVVM
https://www.youtube.com/watch?v=EmUx8wgRxJw&ab_channel=Programaci%C3%B3nAndroidbyAristiDevs
## Repositorio y ViewModel
Creamos un nuevo package de nombre "repository" y dntro una clase GamesRepository
```
// El respositorio utiliza gameApi por lo que hay que inyectar la dependencia, devuelve la lista de resultados,
// comprobamos si todo va bien, la respuesta serán los resultados en caso contrario devolvemos null
class GamesRepository @Inject constructor(private val gameApi:GameApi){
suspend fun getGames(): List<GameList>? {
val response=gameApi.getGames()
if (response.isSuccessful){
return response.body()?.results
}
return null
}
}
```
Creamos por fin una vieja conocida, el package del viewModel y la primera vista GamesViewModel
```
@HiltViewModel
class GamesViewModel @Inject constructor(private val repo: GamesRepository): ViewModel() {
private val _games=MutableStateFlow<List<GameList>> (emptyList())
val games= _games.asStateFlow()
init{
fetchGames()
}
private fun fetchGames(){
viewModelScope.launch {
withContext(Dispatchers.IO){
val result=repo.getGames()
_games.value=result ?: emptyList()
}
}
}
}
```
## Mostrar datos
Vamos a comprobar que todo esto funcione, creamos el package "views" y un archivo HomeView donde por ahora sólo pondremos un lazycolumn
```
@Composable
fun HomeView(viewModel: GamesViewModel){
val games by viewModel.games.collectAsState()
LazyColumn{
items(games) {item->
Text(text = item.name)
}
}
}
```
Ejecutamos para ver que realmente funciona...adelanto, va a dar errores en cuanto a las versiones. Intenten solucionarlo!

Como ves ya estamos consultando el API con internet pero nos falta mejorar las vistas.
## Topbar
Creamos primero el package para los componentes, "components" y dentro el "BodyComponents"
Vamos a usar un color personalizado que añadiremos en las constantes:` const val CUSTOM_BLACK=0xFF2B2626`
Y creamos un topBar genérico para usarlo en todo el proyecto:
```
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainTopBar(title: String, showBackButton:Boolean=false, onClickBackButton:()->Unit){
TopAppBar(
title= {Text(text=title, color= Color.White, fontWeight = FontWeight.ExtraBold)},
colors=TopAppBarDefaults.mediumTopAppBarColors(
containerColor = Color(CUSTOM_BLACK)
),
navigationIcon = {
if (showBackButton){
IconButton(onClick={onClickBackButton()}){
Icon(imageVector= Icons.Default.ArrowBack, contentDescription = null)
}
}
}
)
}
```
Volvemos al HomeView y aquí añadimos el scaffold. moviendo algo el código ya que lo que teníamos antes fue una prueba para ver si estaba todo funcionando:
```
@Composable
fun HomeView(viewModel: GamesViewModel){
Scaffold(
topBar={
MainTopBar(title = "API GAMES") {
}
}
) {
ContentHomeView(viewModel = viewModel, pad = it)
}
}
@Composable
fun ContentHomeView (viewModel:GamesViewModel, pad: PaddingValues){
val games by viewModel.games.collectAsState()
LazyColumn( modifier= Modifier.padding(pad)
){
items(games) {item->
Text(text = item.name)
}
}
}
```
Debería verse algo así:

## Card Game
Vamos a crear un card para mostrar las imágenes que nos descargamos desde la API. Creamos un composable en components->BodyComponents
Es en este componente en el que vamos a usar la librería COIL que nos ayudará a descargar y manejar las imágenes de manera más sencilla.
```
@Composable
fun CardGame(game: GameList, onClick: ()->Unit){
Card (
shape= RoundedCornerShape(5.dp),
modifier= Modifier
.padding(10.dp)
.shadow(40.dp)
.clickable { onClick() }
) {
Column {
MainImage (image=game.background_image)
}
}
}
@Composable
fun MainImage(image:String){
val image = rememberAsyncImagePainter(model = image)
Image (painter=image,
contentDescription=null,
contentScale= ContentScale.Crop,
modifier= Modifier
.fillMaxWidth()
.height(250.dp)
)
}
```
## Mostrar Card
Los componentes que acabamos de crear vamos a mostrarlos en la HomeView, sustituimos donde habíamos puesto el texto de prueba en ContenHomeView por nuestra card.
```
@Composable
fun ContentHomeView (viewModel:GamesViewModel, pad: PaddingValues){
val games by viewModel.games.collectAsState()
LazyColumn( modifier= Modifier
.padding(pad)
.background(Color(CUSTOM_BLACK))
){
items(games) {item->
CardGame(item) {
//aquí pondremos las acciones al hacer click
}
Text( text= item.name,
fontWeight= FontWeight.ExtraBold,
color= Color.White,
modifier= Modifier.padding(start=10.dp)
)
}
}
}
```
Prueben la aplicación y vean todos los detalles que nos proporciona usar el LazyColum.
## Enviar ID
Si queremos que al pulsar sobre un card nos lleve a la descripción del juego correspondiente debemos pasar como parámetro el id del juego a la siguiente view. Para lo cual necesitamos hacer también la navegación entre vistas.
Así que como ya les sonará creamos el package navigation y el archivo NavManager
```
@Composable
fun NavManager(viewModel:GamesViewModel){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "Home"){
composable("Home"){
HomeView(viewModel, navController)
}
composable("DetailView"){
DetailView(viewModel, navController)
}
}
}
```
Como ves tendremos que modificar HomeView para que acepte la navegación y crear DetailView más adelante.
Ahora en el MainActivity debemos llamar a NavManager(viewModel) en lugar de HomeView(viewModel).
Con estos cambios debería funcionar exactamente igual.
Para poder hacer click y que nos lleve al detalle del juego necesitamos pasar ese id como parámetro en la navegación así que en NavManager modificamos:
```
@Composable
fun NavManager(viewModel:GamesViewModel){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "Home"){
composable("Home"){
HomeView(viewModel, navController)
}
composable("DetailView/{id}", arguments= listOf(
navArgument("id"){
type= NavType.IntType
}
)){
val id = it.arguments?.getInt("id")?:0
DetailView(viewModel, navController, id)
}
}
}
```
Habíamos dejado el ContentHomeView preparado para incluir los datos necesarios en la card para poder hacer click así que lo recuperamos:
```
@Composable
fun ContentHomeView (viewModel:GamesViewModel, pad: PaddingValues, navController: NavController){
val games by viewModel.games.collectAsState()
LazyColumn( modifier= Modifier
.padding(pad)
.background(Color(CUSTOM_BLACK))
){
items(games) {item->
CardGame(item) {
navController.navigate("DetailView/${item.id}")
}
Text( text= item.name,
fontWeight= FontWeight.ExtraBold,
color= Color.White,
modifier= Modifier.padding(start=10.dp)
)
}
}
}
```
Para poder mostrar la vista de un solo juego vamos a necesitar generar esta nueva consulta en retrofit.
## Consulta para consumir API por ID
Vamos de nuevo a comenzar, bonito ejercicio de repaso!
Vamos a model y creamos un nuevo modelo data class SingleGameModel.
```
data class SingleGameModel(
val name: String,
val description_raw: String,
val metacritic: Int,
val website: String,
val background_image: String
)
```
El estado de los datos que se muestran se podría hacer a través del ViewModel pero crearemos un package "state" y un data class GameState para controlarlo y guardar los datos que vamos a mostrar.
```
data class GameState(
val name: String="",
val description_raw: String="",
val metacritic: Int=0,
val website: String="",
val background_image: String="")
```
En GameApi es donde tendremos que generar la consulta:
```
interface GameApi {
@GET(ENDPOINT + API_KEY) //formamos el endpoint llamándolos con el método get
suspend fun getGames(): Response<GamesModel> //cómo es una consulta tenemos que
@GET("$ENDPOINT/{id}$API_KEY")
suspend fun getGameById(@Path(value="id")id: Int): Response<SingleGameModel>
}
```
Seguimos el mismo orden y nos faltaría añadir esta consulta al repositori GameRepository:
```
suspend fun getGameById(id:Int): SingleGameModel?{
val response=gameApi.getGameById(id)
if (response.isSuccessful){
return response.body()
}
return null
}
```
Siguiente paso es hacer accesibles estos datos desde el viewModel
```
@HiltViewModel
class GamesViewModel @Inject constructor(private val repo: GamesRepository): ViewModel() {
private val _games=MutableStateFlow<List<GameList>> (emptyList())
val games= _games.asStateFlow()
var state by mutableStateOf(GameState())
private set
init{
fetchGames()
}
private fun fetchGames(){
viewModelScope.launch {
withContext(Dispatchers.IO){
val result=repo.getGames()
_games.value=result ?: emptyList()
}
}
}
fun getGameById(id: Int){
viewModelScope.launch{
withContext(Dispatchers.IO){
val result=repo.getGameById(id)
state=state.copy(
name=result?.name ?: "",
description_raw = result?.description_raw ?: "",
metacritic = result?.metacritic ?: 111,
website = result?.website ?: "",
background_image = result?.background_image ?: ""
)
}
}
}
}
```
Y ya por fin podemos crear la vista del detalle creando el DetailView dentro de views.
```
@Composable
fun DetailView(viewModel: GamesViewModel, navController: NavController, id:Int){
LaunchedEffect(Unit){
viewModel.getGameById(id)
}
Scaffold(
topBar= {
MainTopBar(title = viewModel.state.name, showBackButton = true) {
navController.popBackStack()
}
}
){
ContenDetailView(pad = it, viewModel = viewModel)
}
}
@Composable
fun ContenDetailView(pad: PaddingValues, viewModel: GamesViewModel){
val state= viewModel.state
Column(modifier=Modifier
.padding(pad)
.background(Color(CUSTOM_BLACK))
) {
MainImage(image=state.background_image)
Spacer(modifier=Modifier.height(10.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
. fillMaxWidth()
.padding(start=20.dp, end=5.dp)
){
MetaWebsite(state.website)
}
}
}
```
Como ves hamos llamado un componente MetaWebsite pasando el link a la información del juego, ese componente aún no lo hemos creado...vamos a ello!
## Mostrar sitio web con Intent
Vamos a crear el componente MetaWebsite en BodyComponents, hay que tener en cuenta que contendrá el link a la web con más información sobre el juego, para ello utilizaremos un "intent" que es una manera llamar otras aplicaciones desde nuestra aplicación, en este caso llamaremos al navegador por defecto pasando como parámetro la URL:
https://developer.android.com/guide/components/intents-filters?hl=es-419
En bodycomponents creamos el siguiente composable donde usaremos el intent para abrir la web con la información:
```
@Composable
fun MetaWebsite(url: String){
val context= LocalContext.current
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
Column {
Text(text="METASCORE",
color= Color.White,
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
modifier=Modifier
.padding(top=10.dp, bottom=10.dp)
)
Button(onClick={context.startActivity(intent)}, colors=ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Gray
)
){
Text(text="Sitio Web")
}
}
}
```
Prueba a ejecutar y a pulsar en el link proporcionado y ver qué pasa si les das para atrás.
Intenta mostrar el resto de datos por ti mismo.