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
Publicar un comentario