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

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 {       ...