Ir al contenido principal

Usando Retrofit para Consumir API

Usando Retrofit para Consumir API

La comunicación con servicios web y APIs REST es fundamental en el desarrollo Android. Retrofit simplifica las llamadas de red, Coroutines manejan la asincronía, y un patrón Repositorio/ViewModel estructura el acceso a datos y la lógica de negocio. Para la UI, Jetpack Compose ofrece un enfoque moderno y declarativo para construir interfaces de usuario nativas con Kotlin.

En este post, construiremos una app que muestra la lista de repositorios públicos del usuario de GitHub enelramon. Utilizaremos Retrofit para la llamada a la API, un Repositorio para abstraer la fuente de datos, un ViewModel con StateFlow para gestionar el estado, y Jetpack Compose para dibujar la interfaz de usuario de forma reactiva.


¿Por qué Retrofit, Repositorio, ViewModel y Jetpack Compose?

  • Retrofit: Simplifica las llamadas HTTP, maneja la conversión de JSON y se integra perfectamente con Coroutines.
  • Repositorio: Abstrae la fuente de datos (red). Mejora la separación de responsabilidades y la testeabilidad.
  • ViewModel con StateFlow: Mantiene la lógica de UI y el estado (como RepoUiState), sobreviviendo a cambios de configuración. StateFlow expone el estado de forma eficiente para ser observado por Compose.
  • Coroutines: Manejan operaciones asíncronas (red) de forma estructurada y legible.
  • Jetpack Compose: Es el toolkit moderno de Android para construir UI nativas. Utiliza un enfoque declarativo: describes cómo debería ser tu UI para un estado determinado, y Compose se encarga de actualizarla cuando el estado cambia. Es totalmente interoperable con Kotlin y reduce el boilerplate comparado con XML y Vistas.

Configuración Necesaria

Necesitamos configurar Retrofit, Coroutines, Lifecycle y las dependencias de Jetpack Compose.

Paso 1: Añadir Dependencias de Gradle

Asegúrate de incluir Retrofit, Kotlinx Serialization, OkHttp, Coroutines, Lifecycle (ViewModel) y las dependencias de Jetpack Compose en app/build.gradle.kts.



// build.gradle.kts (Nivel de App)

plugins {

    id("com.android.application")

    id("org.jetbrains.kotlin.android")

    // Plugin de serialización

    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.23" // Usa tu versión de Kotlin

    // Hilt (opcional, si se usa)

    // id("com.google.dagger.hilt.android") version "2.51.1"

    // id("kotlin-kapt") // O ksp

}

android {

    namespace = "com.example.myapp"

    compileSdk = 34 // O la última versión estable

    defaultConfig {

        applicationId = "com.example.myapp"

        minSdk = 24 // Compose generalmente requiere minSdk 21+

        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 {

        // Habilitar Jetpack Compose

        compose = true

    }

    composeOptions {

        // Asegúrate que la versión del compilador de Compose coincida con tu versión de Kotlin

        // Consulta la tabla de compatibilidad: https://developer.android.com/jetpack/androidx/releases/compose-kotlin

        kotlinCompilerExtensionVersion = "1.5.11" // Versión compatible con Kotlin 1.9.23

    }

    packaging {

        resources {

            excludes += "/META-INF/{AL2.0,LGPL2.1}"

        }

    }

}

dependencies {

    // --- Core Android ---

    implementation("androidx.core:core-ktx:1.13.1")

    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") // Para lifecycleScope

    implementation("androidx.activity:activity-compose:1.9.0") // Para setContent en Activity

    // --- Jetpack Compose ---

    // Bill of Materials (BOM) para gestionar versiones de Compose

    val composeBom = platform("androidx.compose:compose-bom:2024.04.01") // Usa la última BOM estable

    implementation(composeBom)

    androidTestImplementation(composeBom)

    // Dependencias de Compose (sin especificar versión, gestionadas por BOM)

    implementation("androidx.compose.ui:ui")

    implementation("androidx.compose.ui:ui-graphics")

    implementation("androidx.compose.ui:ui-tooling-preview") // Para @Preview

    implementation("androidx.compose.material3:material3") // O androidx.compose.material:material si usas Material 2

    // Integración con ViewModel

    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")

    // Tooling para previews y pruebas

    debugImplementation("androidx.compose.ui:ui-tooling")

    androidTestImplementation("androidx.compose.ui:ui-test-junit4")

    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // --- Networking (Retrofit, OkHttp, Kotlinx Serialization) ---

    implementation("com.squareup.retrofit2:retrofit:2.11.0")

    implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")

    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

    implementation("com.squareup.okhttp3:okhttp:4.12.0")

    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // Opcional para logs

    // --- Coroutines ---

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // --- Lifecycle (ViewModel) ---

    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")

    // --- Hilt (Opcional) ---

    // implementation("com.google.dagger:hilt-android:2.51.1")

    // kapt("com.google.dagger:hilt-compiler:2.51.1") // O ksp("...")

    // --- Tests ---

    testImplementation("junit:junit:4.13.2")

    androidTestImplementation("androidx.test.ext:junit:1.1.5")

    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

}

Nota: Verifica y usa las últimas versiones estables compatibles, especialmente la BOM de Compose y la versión del compilador de Compose correspondiente a tu versión de Kotlin. La fecha actual es Sábado, 19 de Abril de 2025. Sincroniza Gradle.

Paso 2: Permiso de Internet

En AndroidManifest.xml (sin cambios):



<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

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

    <application ...> ... </application>

</manifest>

Paso 3: Clase de Datos (Data Class)

Modelo GitHubRepo (sin cambios):



package com.example.myapp.data.model

// ... (código de GitHubRepo igual que antes)

import kotlinx.serialization.SerialName

import kotlinx.serialization.Serializable

@Serializable

data class GitHubRepo(

    @SerialName("id") val id: Long,

    @SerialName("name") val name: String,

    @SerialName("full_name") val fullName: String? = null,

    @SerialName("description") val description: String?,

    @SerialName("html_url") val htmlUrl: String?,

    @SerialName("stargazers_count") val stars: Int,

    @SerialName("forks_count") val forks: Int,

    @SerialName("language") val language: String?

)

Paso 4: Interfaz de la API (Retrofit Service)

Interfaz GitHubApiService (sin cambios):



package com.example.myapp.data.network

// ... (código de GitHubApiService igual que antes)

import com.example.myapp.data.model.GitHubRepo

import retrofit2.http.GET

import retrofit2.http.Path

interface GitHubApiService {

    @GET("users/{user}/repos")

    suspend fun getUserRepos(@Path("user") username: String): List<GitHubRepo> // Escaped: List

}

Paso 5: Instancia de Retrofit

Objeto singleton RetrofitClient (sin cambios):



package com.example.myapp.data.network

// ... (código de RetrofitClient igual que antes)

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory

// ... resto de imports ...

import retrofit2.Retrofit

object RetrofitClient {

    // ... (código interno igual que antes) ...

    val gitHubApiService: GitHubApiService by lazy {

        retrofit.create(GitHubApiService::class.java)

    }

}


Capa de Repositorio con Flow

GithubReposRepository devuelve un Flow (sin cambios respecto a la versión anterior de Flow):



package com.example.myapp.data.repository

// ... (código de GithubReposRepository igual que antes)

import kotlinx.coroutines.flow.Flow

// ... resto de imports ...

@Singleton // Ejemplo

class GithubReposRepository @Inject constructor(

    private val githubApiService: GitHubApiService

) {

    fun getUserRepositories(username: String): Flow<List<GitHubRepo>> = flow { // Escaped: Flow>

        val repos = githubApiService.getUserRepos(username)

        emit(repos)

    }.flowOn(Dispatchers.IO)

}


ViewModel con StateFlow

El RepoViewModel gestiona el estado con StateFlow (sin cambios respecto a la versión anterior de Flow):



package com.example.myapp.ui

import androidx.lifecycle.ViewModel

import androidx.lifecycle.viewModelScope

import com.example.myapp.data.model.GitHubRepo

import com.example.myapp.data.repository.GithubReposRepository

import dagger.hilt.android.lifecycle.HiltViewModel // Ejemplo

import kotlinx.coroutines.flow.*

import kotlinx.coroutines.launch

import javax.inject.Inject // Ejemplo

// Definición de RepoUiState (igual que antes)

sealed class RepoUiState {

    object Idle : RepoUiState()

    object Loading : RepoUiState()

    data class Success(val repos: List<GitHubRepo>) : RepoUiState() // Escaped: List

    data class Error(val message: String) : RepoUiState()

}

@HiltViewModel // Ejemplo

class RepoViewModel @Inject constructor(

    private val repository: GithubReposRepository

) : ViewModel() {

    private val _uiState = MutableStateFlow<RepoUiState>(RepoUiState.Idle) // Escaped: MutableStateFlow

    val uiState: StateFlow<RepoUiState> = _uiState.asStateFlow() // Escaped: StateFlow

    fun fetchUserRepositories(username: String) {

        viewModelScope.launch {

            repository.getUserRepositories(username)

                .onStart { _uiState.value = RepoUiState.Loading }

                .catch { e -> _uiState.value = RepoUiState.Error("Error: ${e.message}") }

                .collect { repoList -> _uiState.value = RepoUiState.Success(repoList) }

        }

    }

    fun fetchEnelramonRepositories() {

        // Evitar recargar si ya está cargando o tiene éxito

        if (_uiState.value is RepoUiState.Idle || _uiState.value is RepoUiState.Error) {

             fetchUserRepositories("enelramon")

        }

    }

}


Construyendo la UI con Jetpack Compose

Ahora reemplazamos la Activity/Fragment basada en Vistas con Composables.

Paso 1: Configurar la Activity Principal

En tu MainActivity, usa setContent para establecer el contenido de la UI con Compose.



package com.example.myapp.ui

import android.os.Bundle

import androidx.activity.ComponentActivity

import androidx.activity.compose.setContent

import androidx.compose.foundation.layout.fillMaxSize

import androidx.compose.material3.MaterialTheme // O material.* si usas M2

import androidx.compose.material3.Surface // O material.* si usas M2

import androidx.compose.ui.Modifier

// import dagger.hilt.android.AndroidEntryPoint // Si usas Hilt

// @AndroidEntryPoint // Si usas Hilt

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContent {

            // Aplicar tema (Material 3 o 2) - Asegúrate de tener un Composable de Tema definido

            // Ejemplo: MyAppTheme { ... }

            MaterialTheme { // Usando Material 3 por defecto

                Surface(

                    modifier = Modifier.fillMaxSize(),

                    color = MaterialTheme.colorScheme.background // M3: colorScheme

                    // color = MaterialTheme.colors.background // M2: colors

                ) {

                    // Llama a tu Composable principal de pantalla

                    RepoScreen()

                }

            }

        }

    }

}

Paso 2: Crear los Composables de la Pantalla

Definimos los Composables para mostrar la pantalla principal, la lista, cada item, el estado de carga y los errores.



package com.example.myapp.ui

import androidx.compose.foundation.layout.*

import androidx.compose.foundation.lazy.LazyColumn

import androidx.compose.foundation.lazy.items

import androidx.compose.material.icons.Icons

import androidx.compose.material.icons.filled.Star // Ejemplo de icono

import androidx.compose.material3.* // O material.* para M2

import androidx.compose.runtime.*

import androidx.compose.ui.Alignment

import androidx.compose.ui.Modifier

import androidx.compose.ui.text.font.FontWeight

import androidx.compose.ui.text.style.TextOverflow

import androidx.compose.ui.unit.dp

import androidx.lifecycle.viewmodel.compose.viewModel // Import para viewModel()

import com.example.myapp.data.model.GitHubRepo

// Composable principal de la pantalla

@Composable

fun RepoScreen(

    // Inyecta el ViewModel usando la extensión de lifecycle-viewmodel-compose

    // Si usas Hilt, esto funciona automáticamente con @HiltViewModel

    viewModel: RepoViewModel = viewModel()

) {

    // Recolecta el StateFlow como un State de Compose. Se recompone automáticamente

    val uiState by viewModel.uiState.collectAsState()

    // Efecto para llamar a la carga inicial solo una vez cuando el Composable entra en la composición

    // o si el estado inicial es Idle/Error.

    LaunchedEffect(key1 = uiState is RepoUiState.Idle || uiState is RepoUiState.Error) {

        if (uiState is RepoUiState.Idle || uiState is RepoUiState.Error) {

            viewModel.fetchEnelramonRepositories()

        }

    }

    // Decide qué mostrar basado en el estado actual

    when (val state = uiState) {

        is RepoUiState.Idle -> {

           // Podrías mostrar un indicador inicial o nada

           LoadingIndicator() // Opcional: Mostrar loading mientras se dispara el LaunchedEffect

        }

        is RepoUiState.Loading -> {

            LoadingIndicator()

        }

        is RepoUiState.Success -> {

            if (state.repos.isEmpty()) {

                EmptyState()

            } else {

                RepoList(repos = state.repos)

            }

        }

        is RepoUiState.Error -> {

            ErrorState(message = state.message)

        }

    }

}

// Composable para el indicador de carga

@Composable

fun LoadingIndicator() {

    Box(

        modifier = Modifier.fillMaxSize(),

        contentAlignment = Alignment.Center

    ) {

        CircularProgressIndicator()

    }

}

// Composable para el estado de error

@Composable

fun ErrorState(message: String) {

    Box(

        modifier = Modifier.fillMaxSize().padding(16.dp),

        contentAlignment = Alignment.Center

    ) {

        Text(

            text = message,

            style = MaterialTheme.typography.bodyLarge, // M3: typography

            // style = MaterialTheme.typography.body1, // M2: typography

            color = MaterialTheme.colorScheme.error // M3: colorScheme

            // color = MaterialTheme.colors.error // M2: colors

        )

    }

}

// Composable para el estado vacío

@Composable

fun EmptyState() {

    Box(

        modifier = Modifier.fillMaxSize().padding(16.dp),

        contentAlignment = Alignment.Center

    ) {

        Text(

            text = "No se encontraron repositorios para este usuario.",

            style = MaterialTheme.typography.bodyLarge

        )

    }

}

// Composable para mostrar la lista de repositorios

@Composable

fun RepoList(repos: List<GitHubRepo>) { // Escaped: List

    // LazyColumn es el equivalente a RecyclerView en Compose

    LazyColumn(

        modifier = Modifier.fillMaxSize(),

        contentPadding = PaddingValues(vertical = 8.dp)

    ) {

        items(

            items = repos,

            key = { repo -> repo.id } // Clave única para cada item (mejora rendimiento)

        ) { repo ->

            RepoItem(repo = repo)

            Divider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant) // M3 Divider

            // Divider(thickness = 0.5.dp, color = Color.LightGray) // M2 Divider simple

        }

    }

}

// Composable para mostrar un único repositorio en la lista

@Composable

fun RepoItem(repo: GitHubRepo) {

    Column(

        modifier = Modifier

            .fillMaxWidth()

            .padding(horizontal = 16.dp, vertical = 12.dp) // Ajusta el padding

    ) {

        Text(

            text = repo.name,

            style = MaterialTheme.typography.titleMedium, // M3

            // style = MaterialTheme.typography.h6, // M2

            fontWeight = FontWeight.Bold,

            maxLines = 1,

            overflow = TextOverflow.Ellipsis

        )

        Spacer(modifier = Modifier.height(4.dp))

        Text(

            text = repo.description ?: "Sin descripción",

            style = MaterialTheme.typography.bodyMedium, // M3

            // style = MaterialTheme.typography.body2, // M2

            maxLines = 2,

            overflow = TextOverflow.Ellipsis,

            color = LocalContentColor.current.copy(alpha = 0.7f) // Color secundario

        )

        Spacer(modifier = Modifier.height(8.dp))

        Row(

            verticalAlignment = Alignment.CenterVertically,

            horizontalArrangement = Arrangement.spacedBy(16.dp) // Espacio entre elementos de la fila

        ) {

            // Estrellas

            Row(verticalAlignment = Alignment.CenterVertically) {

                Icon(

                    imageVector = Icons.Default.Star,

                    contentDescription = "Estrellas",

                    modifier = Modifier.size(16.dp),

                    tint = MaterialTheme.colorScheme.onSurfaceVariant // M3

                    // tint = LocalContentColor.current.copy(alpha = 0.6f) // M2

                )

                Spacer(modifier = Modifier.width(4.dp))

                Text(

                    text = repo.stars.toString(),

                    style = MaterialTheme.typography.bodySmall // M3

                    // style = MaterialTheme.typography.caption // M2

                )

            }

            // Lenguaje (si existe)

            repo.language?.let { lang ->

                 Text(

                     text = lang,

                     style = MaterialTheme.typography.bodySmall // M3

                    // style = MaterialTheme.typography.caption // M2

                 )

            }

        }

    }

}

N

Comentarios

Entradas populares de este blog

Principios SOLID del Tio Bob

  Principios SOLID del Tio Bob Los principios sólidos son un conjunto de pautas de diseño de software que se centran en lograr código limpio, modular y mantenible. Estos principios fueron propuestos por Robert C. Martin (Uncle Bob) y se consideran fundamentales en el desarrollo de software orientado a objetos. Los cinco principios sólidos son los siguientes:   1. Principio de Responsabilidad Única (Single Responsibility Principle, SRP): Una clase debería tener una única responsabilidad. Esto significa que una clase debe tener una única razón para cambiar. Al tener una responsabilidad única, se logra un código más cohesivo y fácil de mantener.   2. Principio de Abierto/Cerrado (Open/Closed Principle, OCP): Las entidades de software (clases, módulos, etc.) deben estar abiertas para su extensión pero cerradas para su modificación. Esto significa que el comportamiento de una entidad puede ser extendido sin necesidad de modificar su código fuente original.  ...

Desarrollador junior C#

Habilidades que se esperan de un desarrollador junior (C#)   1.      Conocimientos básicos de programación: Debes tener una comprensión sólida de los conceptos fundamentales de programación, como variables, estructuras de control, bucles, funciones, etc. 2.      Dominio del lenguaje C#: Debes tener conocimientos sólidos del lenguaje C# y su sintaxis. Debes estar familiarizado con los conceptos orientados a objetos, como clases, herencia, polimorfismo, etc. 3.      Conocimientos de .NET Framework: C# se utiliza principalmente para el desarrollo en el entorno de .NET Framework, por lo que debes tener un conocimiento básico de esta plataforma, incluyendo las bibliotecas y clases comunes que se utilizan en el desarrollo de aplicaciones. 4.      Experiencia con Visual Studio: Visual Studio es el entorno de desarrollo integrado (IDE) más popular para C#. Debes estar familiarizado con su uso y ser capa...

Programación asíncrona comparación Kotlin y C#

La programación asíncrona es un enfoque en la programación que permite que las tareas se ejecuten de manera independiente y no bloqueante. En lugar de esperar a que una tarea se complete antes de pasar a la siguiente, las tareas se pueden ejecutar en paralelo o de manera secuencial, lo que mejora la eficiencia y la capacidad de respuesta de las aplicaciones. ¿Como implementar programación asíncrona en Kotlin? Usando Coroutines. Las coroutines en Kotlin son una forma de escribir código asíncrono de manera más concisa y legible. En lugar de bloquear el hilo principal mientras esperamos a que se complete una tarea, las coroutines permiten que el hilo siga ejecutándose mientras esperamos que una tarea asincrónica termine. Esto mejora la eficiencia y la capacidad de respuesta de las aplicaciones. Un ejemplo básico: KOTLIN import kotlinx.coroutines.* fun main() {     println("Inicio")     // Lanzar una coroutine     GlobalScope.launch {       ...