Ir al contenido principal

Usando Ktorfit para Consumir API

Usando Ktorfit para Consumir API con Jetpack Compose

En el desarrollo Android moderno, consumir APIs REST es una tarea diaria. Si bien Retrofit ha sido el estándar por mucho tiempo, alternativas como Ktorfit están ganando popularidad. Ktorfit, construido sobre Ktor Client, ofrece una forma de definir APIs de manera declarativa, muy similar a Retrofit, pero aprovechando el poder y la flexibilidad de Ktor.

En este post, veremos cómo construir una app que muestra la lista de repositorios públicos del usuario de GitHub enelramon. Usaremos Ktorfit para las llamadas a la API, un patrón Repositorio, un ViewModel con StateFlow para gestionar el estado, y Jetpack Compose para la interfaz de usuario.


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

  • Ktorfit: Permite definir APIs REST de forma type-safe usando anotaciones, igual que Retrofit, pero utiliza Ktor Client como motor de red subyacente. Esto lo hace una opción natural si ya usas Ktor en tu proyecto o buscas capacidades multiplataforma.
  • Ktor Client: Es el cliente HTTP de Kotlin, potente, flexible y diseñado con Coroutines en mente. Es la base sobre la que opera Ktorfit.
  • Repositorio: Abstrae la fuente de datos (red vía Ktorfit/Ktor Client). Mejora la separación de responsabilidades y la testeabilidad.
  • ViewModel con StateFlow: Mantiene la lógica de UI y el estado (RepoUiState), sobreviviendo a cambios de configuración y exponiendo el estado de forma reactiva a Compose.
  • Coroutines: Esenciales para manejar las operaciones asíncronas de Ktor Client y Ktorfit.
  • Jetpack Compose: El toolkit moderno y declarativo para construir UI nativas en Android con Kotlin.

Configuración Necesaria

Necesitamos configurar Ktorfit, Ktor Client, Coroutines, Lifecycle y las dependencias de Jetpack Compose.

Paso 1: Añadir Dependencias de Gradle

Actualiza app/build.gradle.kts para incluir Ktorfit, Ktor Client (con un motor como OkHttp o Android, y serialización), y quitar las de Retrofit.


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

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.23" // Ktor usa Kotlinx Serialization
    // Hilt (opcional)
    // id("com.google.dagger.hilt.android") version "2.51.1"
    // id("kotlin-kapt") // O ksp
    // KSP para Ktorfit (si usas el procesador de anotaciones, opcional pero recomendado para rendimiento)
    // id("com.google.devtools.ksp") version "1.9.23-1.0.19" // Versión compatible con Kotlin
}

android {
    // ... (configuración igual que antes: namespace, compileSdk, defaultConfig, etc.) ...

    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.11" // Compatible con Kotlin 1.9.23
    }
    // ... (packagingOptions igual que antes) ...
}

dependencies {
    // --- Core Android & Lifecycle ---
    implementation("androidx.core:core-ktx:1.13.1")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    implementation("androidx.activity:activity-compose:1.9.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")

    // --- Jetpack Compose ---
    val composeBom = platform("androidx.compose:compose-bom:2024.04.01")
    implementation(composeBom)
    androidTestImplementation(composeBom)
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    debugImplementation("androidx.compose.ui:ui-tooling")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // --- Ktorfit ---
    implementation("de.jensklingenberg.ktorfit:ktorfit-lib:1.13.0") // Revisa la última versión de Ktorfit
    // Si usas KSP (opcional pero recomendado):
    // ksp("de.jensklingenberg.ktorfit:ktorfit-ksp:1.13.0")

    // --- Ktor Client ---
    val ktorVersion = "2.3.10" // Revisa la última versión de Ktor
    implementation("io.ktor:ktor-client-core:$ktorVersion")
    // Elige un motor para Ktor (usaremos OkHttp para poder usar interceptores fácilmente)
    implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
    // Logging para Ktor (se configura a través del motor OkHttp)
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // Mantenemos el interceptor de OkHttp

    // --- Ktor Content Negotiation & Serialization ---
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    // Dependencia runtime de Kotlinx Serialization (ya incluida indirectamente, pero por claridad)
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

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

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

    // --- 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: Asegúrate de usar las últimas versiones compatibles de Ktorfit, Ktor y el compilador KSP si lo usas. Sincroniza Gradle. La fecha actual es Sábado, 19 de Abril de 2025.

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, sigue usando `@Serializable` de Kotlinx):


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,
    // ... resto de campos ...
)

Paso 4: Interfaz de la API (¡Prácticamente sin cambios!)

La definición de la interfaz GitHubApiService es casi idéntica a la de Retrofit, gracias a Ktorfit.


package com.example.myapp.data.network

import com.example.myapp.data.model.GitHubRepo
// Importaciones de Ktorfit en lugar de Retrofit
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Path

interface GitHubApiService {
    // Las anotaciones son de Ktorfit pero se ven iguales
    @GET("users/{user}/repos")
    suspend fun getUserRepos(@Path("user") username: String): List<GitHubRepo> // Escaped: List
}

Paso 5: Crear la Instancia de Ktorfit y Ktor Client

Aquí es donde configuramos Ktor Client y luego lo usamos para construir Ktorfit.


package com.example.myapp.data.network

import com.example.myapp.data.model.GitHubRepo // Asegúrate que el modelo está importado
import de.jensklingenberg.ktorfit.Ktorfit
import io.ktor.client.*
import io.ktor.client.engine.okhttp.* // Usando el motor OkHttp
import io.ktor.client.plugins.contentnegotiation.* // Para JSON
import io.ktor.serialization.kotlinx.json.* // Para JSON con Kotlinx
import kotlinx.serialization.json.Json
import okhttp3.logging.HttpLoggingInterceptor // Para logging

object KtorfitClient {
    private const val BASE_URL = "https://api.github.com/"

    // 1. Configurar el interceptor de logging (igual que antes)
    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    // 2. Configurar el Ktor HttpClient
    private val httpClient = HttpClient(OkHttp) { // Usar el motor OkHttp
        engine {
            // Configuración específica del motor OkHttp
            addInterceptor(loggingInterceptor)
            // Aquí puedes añadir otros interceptores de OkHttp si los necesitas
        }

        // Instalar y configurar ContentNegotiation para manejar JSON
        install(ContentNegotiation) {
            json(Json {
                prettyPrint = true      // Opcional: para logs más legibles
                isLenient = true        // Opcional: ser más flexible con el JSON
                ignoreUnknownKeys = true // Importante para ignorar campos no definidos en el modelo
            })
        }

        // Puedes añadir otras configuraciones de Ktor Client aquí
        // como defaultRequest, etc.
    }

    // 3. Construir Ktorfit usando el HttpClient configurado
    private val ktorfit = Ktorfit.Builder()
        .baseUrl(BASE_URL)
        .httpClient(httpClient)
        .build()

    // 4. Crear la instancia del servicio API
    val gitHubApiService: GitHubApiService by lazy {
        ktorfit.create<GitHubApiService>() // Usar create() en Ktorfit
    }
}

Capa de Repositorio (Sin cambios lógicos)

El Repositorio sigue dependiendo de la interfaz `GitHubApiService` y devolviendo un `Flow`. Su implementación interna no cambia, ya que sigue llamando a una función `suspend`.


package com.example.myapp.data.repository

import com.example.myapp.data.model.GitHubRepo
import com.example.myapp.data.network.GitHubApiService // La interfaz es la misma
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
import javax.inject.Singleton

@Singleton // Ejemplo
class GithubReposRepository @Inject constructor(
    // Inyecta la instancia del servicio creada por KtorfitClient
    private val githubApiService: GitHubApiService
) {
    fun getUserRepositories(username: String): Flow<List<GitHubRepo>> = flow { // Escaped: Flow>
        // La llamada al método suspend es igual
        val repos = githubApiService.getUserRepos(username)
        emit(repos)
    }.flowOn(Dispatchers.IO)
}

ViewModel con StateFlow (Sin cambios)

El `RepoViewModel` no necesita cambios, ya que su dependencia es el `GithubReposRepository`, y la interfaz de este no ha cambiado. Sigue gestionando `RepoUiState` con `StateFlow`.


package com.example.myapp.ui
// ... (Imports y código del ViewModel exactamente igual que en la versión con Compose anterior) ...

sealed class RepoUiState { /* ... */ }

@HiltViewModel // Ejemplo
class RepoViewModel @Inject constructor(
    private val repository: GithubReposRepository // Depende del repositorio, no del cliente de red
) : ViewModel() {

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

    // ... (Las funciones fetchUserRepositories y fetchEnelramonRepositories son iguales) ...
}

Construyendo la UI con Jetpack Compose (Sin cambios)

La capa de UI de Jetpack Compose tampoco necesita cambios. Sigue observando el `StateFlow` del `RepoViewModel` y reaccionando a los cambios de `RepoUiState`.

Paso 1: Configurar la Activity Principal

MainActivity.kt sigue usando `setContent` y llamando a `RepoScreen`.


package com.example.myapp.ui
// ... (Código de MainActivity exactamente igual que en la versión con Compose anterior) ...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    RepoScreen() // Sin cambios aquí
                }
            }
        }
    }
}

Paso 2: Crear los Composables de la Pantalla

Los composables `RepoScreen`, `LoadingIndicator`, `ErrorState`, `EmptyState`, `RepoList`, y `RepoItem` son exactamente los mismos que en la versión anterior, ya que dependen únicamente del `RepoUiState` proporcionado por el ViewModel.


package com.example.myapp.ui
// ... (Imports de Compose y código de RepoScreen, LoadingIndicator, ErrorState, EmptyState, RepoList, RepoItem
//      exactamente igual que en la versión con Compose anterior) ...

@Composable
fun RepoScreen(viewModel: RepoViewModel = viewModel()) { /* ... Mismo código ... */ }

@Composable
fun LoadingIndicator() { /* ... Mismo código ... */ }

@Composable
fun ErrorState(message: String) { /* ... Mismo código ... */ }

@Composable
fun EmptyState() { /* ... Mismo código ... */ }

@Composable
fun RepoList(repos: List<GitHubRepo>) { /* ... Mismo código ... */ } // Escaped

@Composable
fun RepoItem(repo: GitHubRepo) { /* ... Mismo código ... */ }

Conclusión

Hemos adaptado nuestra aplicación para usar Ktorfit como cliente de red en lugar de Retrofit. Como hemos visto, el mayor cambio reside en la configuración de las dependencias y la creación de la instancia del cliente (usando Ktor Client). Sin embargo, gracias a que Ktorfit imita la definición de interfaces de Retrofit, las propias interfaces de API, el Repositorio, el ViewModel y toda la capa de UI con Jetpack Compose permanecieron prácticamente sin cambios.

Ktorfit ofrece una excelente alternativa para quienes prefieren o necesitan usar Ktor Client, proporcionando una transición suave desde Retrofit y manteniendo una arquitectura limpia y reactiva con StateFlow y Jetpack Compose.

Recursos Adicionales:

Comentarios

Entradas populares de este blog

De Coverlet a JaCoCo: Trayendo la magia de la cobertura a Android Studio

Todo comenzó un día cualquiera, mientras revisaba un reporte de cobertura de pruebas generado por Coverlet para un proyecto en .NET. Me quedé fascinado por el nivel de detalle y claridad que proporcionaba: saber exactamente qué partes del código estaban cubiertas por las pruebas, y cuáles no. Fue entonces cuando me pregunté: ¿Y si pudiera tener algo así en mi proyecto de Android con Kotlin y Jetpack Compose? La Chispa Inicial ✨ Como desarrollador, siempre busco mejorar la calidad de mi código, y contar con herramientas que me permitan medir la cobertura de pruebas es clave. Después de una rápida búsqueda, me topé con **JaCoCo**, una herramienta muy popular para medir cobertura de código en proyectos Java y Kotlin. ¡Lo mejor de todo es que es compatible con Android Studio! Mi objetivo estaba claro: debía integrar JaCoCo en mi proyecto de Android para tener reportes detallados de cobertura, tal como lo había visto en Coverlet. El Desafío: Configurar JaCoCo en un Proyecto Android 🛠️ El p...

Guía Definitiva de Estrategias de Branching en Git: Elige la Mejor para tu Equipo

Imagina esta situación: lideras un equipo de desarrollo y todos están enviando código al mismo tiempo. Sin una estrategia de ramas (branching) clara, tu base de código se convierte rápidamente en un caos de conflictos, compilaciones rotas y desarrolladores frustrados. ¿Te suena familiar? Si alguna vez te has encontrado en este lío, no estás solo. Una estrategia de branching en Git es el mapa que tu equipo necesita para gestionar los cambios de código, colaborar de manera efectiva y entregar software de calidad. Es, en esencia, un conjunto de reglas que define cómo los desarrolladores interactúan con el repositorio, cuándo crear ramas, cómo fusionar cambios y cómo mantener la estabilidad del código en todo el ciclo de vida del desarrollo. Pero es más que solo reglas. Una buena estrategia de branching se alinea con la forma en que trabaja tu equipo: tus ciclos de lanzamiento, los flujos de trabajo de QA, tus pipelines de CI/CD e incluso la frecuencia con la que necesitas aplicar correcc...

Inyección de Dependencias usando Hilt

Inyección de Dependencias usando Hilt La Inyección de Dependencias (DI) es fundamental para construir aplicaciones Android robustas, escalables y fáciles de testear. Mientras que existen varias librerías para lograrlo, Hilt se ha establecido como la solución recomendada y estándar de Google. Construido sobre la potencia de Dagger, Hilt simplifica enormemente la implementación de DI en Android. Si buscas una forma estandarizada, con menos boilerplate que Dagger puro y con excelente integración con los componentes de Android Jetpack, Hilt es la respuesta. ¡Descubramos cómo funciona! ¿Por qué elegir Hilt? Hilt ofrece ventajas significativas para el desarrollo Android: Estándar de Android: Es la librería DI recomendada por Google, lo que asegura buena documentación, soporte y alineación con las prácticas modernas de Android. Menos Boilerplate (vs. Dagger): Reduce drásticamente el código de configuración necesario en comparación con usar Dagger directamente en Andr...