tutoriales.com

Maestría en Inyección de Dependencias en Android con Hilt y KSP

Descubre cómo implementar y escalar la inyección de dependencias en tus proyectos Android nativos utilizando Dagger Hilt y el compilador KSP para acelerar los tiempos de compilación.

Avanzado12 min de lectura2 views
Reportar error

🚀 Introducción a la Inyección de Dependencias Moderna en Android

La inyección de dependencias (DI) es un pilar fundamental en el desarrollo de software moderno. Nos permite desacoplar componentes, facilitar las pruebas unitarias y mantener bases de código limpias y escalables. Durante años, Dagger 2 fue el estándar de oro en Android, pero su pronunciada curva de aprendizaje y la verbosidad de su configuración lo hacían difícil de adoptar para muchos equipos. Con la llegada de Hilt, Google simplificó drásticamente este proceso, construyendo una abstracción estándar sobre Dagger.

Además, la transición del compilador de anotaciones tradicional (kapt) hacia KSP (Kotlin Symbol Processing) ha supuesto una revolución en términos de rendimiento de compilación. En este tutorial completo, aprenderemos a configurar Hilt utilizando KSP en un entorno de desarrollo Android moderno con Kotlin, abarcando desde conceptos básicos hasta casos de uso avanzados como inyección en ViewModels, BroadcastReceivers y pruebas automatizadas.

💡 Consejo: Asegúrate de utilizar la versión más reciente de Android Studio y Kotlin para aprovechar al máximo las mejoras de rendimiento que ofrece KSP sobre kapt.

🛠️ Configuración del Entorno y Dependencias

Antes de escribir código, debemos preparar nuestro proyecto para soportar Hilt y KSP. A diferencia del kapt tradicional, KSP procesa los símbolos de Kotlin directamente, lo que puede reducir los tiempos de compilación hasta en un 50% en proyectos grandes.

1. Configuración en el archivo build.gradle.kts a nivel de proyecto

Primero, añade los plugins necesarios en el archivo raíz de tu proyecto:

plugins {
    id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
    id("com.google.dagger.hilt.android") version "2.51" apply false
}

2. Configuración en el módulo app/build.gradle.kts

A continuación, aplica los plugins e implementa las dependencias requeridas en el módulo de la aplicación:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp")
    id("com.google.dagger.hilt.android")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.51")
    ksp("com.google.dagger:hilt-compiler:2.51")
    
    // Dependencias adicionales para Jetpack Navigation y ViewModels si es necesario
    implementation("androidx.hilt:hilt-navigation-fragment:1.2.0")
}
Flujo de Arquitectura KSP + Hilt Código Fuente @HiltAndroidApp @Inject, @Module Motor KSP Procesador Hilt Código Generado Hilt_MyApp.kt _HiltModules.java Compilador Kotlin Símbolos 1. Escaneo 2. Generación 3. Compilación de todo

🏗️ Estructura Base y la Clase Application

Todo proyecto que utilice Hilt Obligatorio debe contar con una clase Application anotada con @HiltAndroidApp. Esto le indica a Hilt que debe generar la infraestructura de componentes raíz para toda la aplicación.

Creemos nuestra clase base:

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MiAppAndroid : Application() {
    override fun onCreate() {
        super.onCreate()
        // Inicializaciones globales como librerías de terceros, Timber, etc.
    }
}

No olvides registrar esta clase en tu archivo AndroidManifest.xml dentro de la etiqueta <application>:

<application
    android:name=".MiAppAndroid"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.MiApp">
    <!-- Actividades y servicios -->
</application>

📦 Proveyendo Dependencias con Módulos y @Module

¿Qué sucede cuando queremos inyectar clases de terceros (como Retrofit, OkHttpClient o instancias de Room Database) que no podemos modificar directamente con la anotación @Inject constructor()? Aquí es donde entran en juego los Módulos de Hilt.

Un módulo es una clase anotada con @Module y @InstallIn. La anotación @InstallIn define en qué componente de Android vivirá el contenedor de dependencias provisto por ese módulo.

Ejemplo práctico: Proveiendo una instancia de Retrofit

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.ejemplo.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}
⚠️ Advertencia: El alcance (scope) `@Singleton` asegura que solo exista una única instancia de la dependencia durante todo el ciclo de vida de la aplicación. Úsalo con precaución para evitar pérdidas de memoria.

🔄 Manejo de Interfaces con @Binds

Cuando trabajamos con principios SOLID, especialmente el principio de inversión de dependencias, es común programar contra interfaces y no contra implementaciones concretas. Mientras que @Provides se utiliza para inicializar objetos, @Binds es la forma optimizada de Hilt para decirle qué implementación debe usar cuando se solicita una interfaz.

Imagina que tenemos un repositorio:

interface UsuarioRepository {
    fun obtenerNombreUsuario(): String
}

class UsuarioRepositoryImpl @Inject constructor(
    private val apiService: ApiService
) : UsuarioRepository {
    override fun obtenerNombreUsuario(): String {
        return "Usuario de Prueba"
    }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    abstract fun bindUsuarioRepository(
        usuarioRepositoryImpl: UsuarioRepositoryImpl
    ): UsuarioRepository
}

📱 Inyección en Componentes de Android

Hilt facilita la inyección automática en prácticamente todos los componentes del framework de Android utilizando la anotación @AndroidEntryPoint.

1. Inyección en Actividades y Fragmentos

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    // ¡Nunca uses lateinit var con visibilidad private para inyección de campos!
    @Inject
    lateinit var analyticsHelper: AnalyticsHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        analyticsHelper.registrarEvento("MainActivity_Abierta")
    }
}

2. Inyección en ViewModels

Para los ViewModels, no utilizamos @Inject directamente en los campos de la clase, sino en el constructor, acompañado de la anotación @HiltViewModel:

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
    private val usuarioRepository: UsuarioRepository
) : ViewModel() {

    fun getNombre(): String {
        return usuarioRepository.obtenerNombreUsuario()
    }
}

Luego, puedes instanciar el ViewModel de manera estándar en tu Actividad o Fragmento usando el delegado de Kotlin:

private val viewModel: MainViewModel by viewModels()
Actividad Hilt Factory ViewModelComponent Repositorio ViewModel 1. by viewModels() 2. Solicitar Grafo 3. Obtener Repo Retornar Instancia 4. Inyectar Constructor(Repo) 5. ViewModel listo para usar

🧪 Pruebas Automatizadas con Hilt

Una de las mayores ventajas de utilizar Hilt es la facilidad con la que permite reemplazar dependencias reales por mocks o fakes durante las pruebas instrumentadas. Para pruebas de UI con Espresso, utilizamos la anotación @HiltAndroidTest.

Ejemplo de prueba instrumentada

import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var analyticsHelper: AnalyticsHelper

    @Before
    fun init() {
        hiltRule.inject()
    }

    @Test
    fun testInyeccionCorrecta() {
        // Verificar que la dependencia no sea nula
        assert(analyticsHelper != null)
    }
}

📊 Comparativa de Rendimiento: KAPT vs KSP

Para entender por qué KSP es el estándar moderno en proyectos con Hilt, revisemos la siguiente tabla comparativa:

| Característica | KAPT (Kotlin Annotation Processing) | KSP (Kotlin Symbol Processing) | |---|---|---|>

Velocidad de CompilaciónLenta (traduce código a Java)Rápida (procesamiento directo de Kotlin)
Soporte OficialEn modo mantenimiento / DepreciadoActivo y recomendado por Google
Consumo de MemoriaAltoOptimizado
Compatibilidad con IDERegularExcelente integración con IntelliJ / Android Studio

❓ Preguntas Frecuentes (FAQ)

¿Puedo usar Hilt en un proyecto multi-módulo? Sí, Hilt está diseñado específicamente para arquitecturas modulares. Cada módulo de características (*feature module*) puede tener sus propios componentes y dependencias siempre que estén correctamente enlazados con los componentes raíz.
¿Qué pasa si intento inyectar un campo privado? Hilt arrojará un error en tiempo de compilación. Los campos inyectados con `@Inject` en actividades o fragmentos deben tener visibilidad pública o package-private, pero nunca `private`.
¿Es obligatorio usar KSP con Hilt? No es estrictamente obligatorio, ya que Hilt aún soporta KAPT, pero Google recomienda encarecidamente migrar a KSP para aprovechar las mejoras drásticas en los tiempos de compilación.

🎯 Conclusión

La combinación de Hilt y KSP representa el estado del arte en la inyección de dependencias para Android nativo. Nos permite mantener bases de código desacopladas, altamente testeables y con tiempos de compilación reducidos. Al seguir buenas prácticas como el uso de @Binds para interfaces, la correcta gestión de ámbitos mediante @Singleton, y la integración nativa con ViewModels, estarás preparado para construir aplicaciones robustas de nivel de producción.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!