Ir al contenido principal

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 Android.
  • Seguridad en Tiempo de Compilación: Al igual que Dagger, Hilt valida las dependencias en tiempo de compilación, detectando errores antes de ejecutar la app.
  • Integración con Jetpack: Proporciona extensiones directas para inyectar fácilmente componentes como ViewModels, WorkManager, Navigation y más.
  • Ciclos de Vida y Scoping Simplificados: Define componentes y scopes estándar ligados al ciclo de vida de Android (@Singleton, @ActivityScoped, @ViewModelScoped, etc.), facilitando la gestión del tiempo de vida de las dependencias.
  • Basado en Dagger: Aprovecha la eficiencia y el rendimiento comprobados de Dagger bajo el capó.

Configurando Hilt en tu Proyecto Android

Integrar Hilt requiere unos pocos pasos de configuración inicial:

Paso 1: Añadir Dependencias y Plugin de Gradle

Necesitas configurar el plugin de Hilt y añadir las dependencias necesarias. Usaremos KSP (Kotlin Symbol Processing) que es más rápido que KAPT, aunque KAPT también funciona.

1. Archivo build.gradle.kts (Nivel de Proyecto):


// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id("com.android.application") version "8.X.X" apply false // Usa tu versión
    id("org.jetbrains.kotlin.android") version "1.9.XX" apply false // Usa tu versión
    // Plugin de Hilt
    id("com.google.dagger.hilt.android") version "2.51.1" apply false // Usa la última versión estable de Hilt
    // Plugin KSP (Recomendado)
    id("com.google.devtools.ksp") version "1.9.XX-1.0.XX" apply false // Usa la versión KSP compatible con tu Kotlin
}

2. Archivo build.gradle.kts (Nivel de App):


plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    // Aplicar plugin KSP (o kotlin-kapt si usas KAPT)
    id("com.google.devtools.ksp")
    // Aplicar plugin Hilt
    id("com.google.dagger.hilt.android")
    // Opcional: si usas kapt en lugar de ksp
    // id("kotlin-kapt")
}

android {
    // ... otras configuraciones (compileSdk, defaultConfig, etc.)
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8 // O superior
        targetCompatibility = JavaVersion.VERSION_1_8 // O superior
    }
    kotlinOptions {
        jvmTarget = "1.8" // O superior
    }
}

dependencies {
    // Dependencias de Hilt
    implementation("com.google.dagger:hilt-android:2.51.1") // Usa la misma versión que el plugin
    ksp("com.google.dagger:hilt-compiler:2.51.1") // Para KSP
    // Si usas KAPT en lugar de KSP:
    // kapt("com.google.dagger:hilt-compiler:2.51.1")

    // (Opcional) Para integración con ViewModels en Jetpack Compose Navigation
    implementation("androidx.hilt:hilt-navigation-compose:1.2.0") // Usa la última versión

    // Otras dependencias de tu app (androidx.core, appcompat, material, compose, etc.)
}

// Opcional: si usas kapt
// kapt {
//    correctErrorTypes = true
// }

Nota: Reemplaza las versiones con las últimas estables disponibles. Sincroniza tu proyecto Gradle después de los cambios.

Paso 2: Anotar la Clase Application

Hilt necesita una clase Application anotada con @HiltAndroidApp para iniciar la generación de código.


package com.example.myapp

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

@HiltAndroidApp
class MyApp : Application() {
    // Puedes dejarla vacía o añadir lógica de inicialización adicional si es necesario
    override fun onCreate() {
        super.onCreate()
        // ...
    }
}

No olvides declarar esta clase en tu AndroidManifest.xml:


<manifest ...>
    <application
        android:name=".MyApp"
        ... >
        <!-- otras configuraciones -->
    </application>
</manifest>

Paso 3: Anotar Componentes Android y Definir Dependencias

Para que Hilt pueda inyectar dependencias en tus Activities, Fragments, Services, etc., debes anotarlos con @AndroidEntryPoint.

Para definir cómo crear las dependencias, Hilt usa principalmente:

  • @Inject constructor: La forma preferida. Anota el constructor de tu clase. Hilt sabrá cómo crearla siempre que también sepa crear sus parámetros.
  • Módulos Hilt (@Module y @InstallIn): Para casos donde la inyección por constructor no es posible (interfaces, clases externas, configuración compleja).
    • @Binds: Dentro de un módulo, para indicar a Hilt qué implementación usar para una interfaz (más eficiente que @Provides).
    • @Provides: Dentro de un módulo, para indicar cómo construir instancias de tipos que no controlas (ej. de librerías externas como Retrofit, Room) o cuando necesitas lógica de construcción.
    • @InstallIn(Component::class): Indica en qué componente de Hilt (y por tanto, con qué scope) se instalará el módulo (ej: SingletonComponent::class, ActivityComponent::class, ViewModelComponent::class).

Ejemplo de Módulo:


package com.example.myapp.di

import com.example.myapp.data.UserRepository
import com.example.myapp.data.UserRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class) // Disponible durante toda la vida de la app
abstract class AppModule {

    // Usamos @Binds para decirle a Hilt que cuando se pida un UserRepository,
    // debe proveer una instancia de UserRepositoryImpl.
    @Binds
    @Singleton // La instancia será singleton
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository

    // Si necesitaras @Provides (ejemplo):
    // @Provides
    // @Singleton
    // fun provideApiService(): ApiService {
    //     return Retrofit.Builder().baseUrl("...").build().create(ApiService::class.java)
    // }
}

// --- Clases de ejemplo ---
package com.example.myapp.data

import javax.inject.Inject
import javax.inject.Singleton

interface UserRepository {
    suspend fun getUser(id: String): String
}

// Hilt sabe cómo crear UserRepositoryImpl porque usamos @Inject en su constructor.
// @Singleton aquí no es estrictamente necesario si ya lo definimos en @Binds, pero puede ser útil.
@Singleton
class UserRepositoryImpl @Inject constructor() : UserRepository {
    override suspend fun getUser(id: String): String {
        kotlinx.coroutines.delay(500) // Simular trabajo
        return "Hilt: Datos del Usuario $id"
    }
}

Inyectando Dependencias con Hilt

En Activities, Fragments, Services (Anotados con @AndroidEntryPoint):

Usa la anotación @Inject en campos (normalmente lateinit var). Hilt los inicializará por ti.

Para ViewModels, usa el delegado estándar by viewModels(); Hilt se integra automáticamente.


package com.example.myapp.ui

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels // Importar delegado estándar
import com.example.myapp.data.UserRepository // Podrías inyectarlo aquí, pero es mejor via ViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint // ¡Esencial para que Hilt inyecte aquí!
class MainActivity : AppCompatActivity() {

    // Inyectar ViewModel (Hilt maneja la factoría automáticamente)
    private val myViewModel: MyViewModel by viewModels()

    // También puedes inyectar otros tipos definidos en Hilt directamente
    // @Inject lateinit var userRepository: UserRepository // Ejemplo, aunque usualmente se accede via ViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(...)

        // Usar el ViewModel
        myViewModel.loadUserData()

        // Observar datos del ViewModel
        // myViewModel.userDataFlow.observe...
    }
}

En ViewModels:

Anota la clase ViewModel con @HiltViewModel y usa @Inject constructor para recibir sus dependencias.


package com.example.myapp.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapp.data.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel // Anotación clave
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject // Para el constructor

@HiltViewModel // Indica a Hilt cómo proveer este ViewModel
class MyViewModel @Inject constructor( // Hilt inyectará UserRepository aquí
    private val userRepository: UserRepository
) : ViewModel() {

    private val _userDataFlow = MutableStateFlow("Hilt: Inicial...")
    val userDataFlow: StateFlow = _userDataFlow

    fun loadUserData() {
        viewModelScope.launch {
            try {
                _userDataFlow.value = "Hilt: Cargando..."
                val user = userRepository.getUser("hiltUser")
                _userDataFlow.value = user
            } catch (e: Exception) {
                 _userDataFlow.value = "Hilt: Error - ${e.message}"
            }
        }
    }
}

En Jetpack Compose:

La forma principal de obtener dependencias en Composables es a través de un ViewModel inyectado con Hilt.

Usa la función Composable hiltViewModel() que proviene de la dependencia androidx.hilt:hilt-navigation-compose (si necesitas que el ViewModel esté asociado a un destino de Navigation Compose) o simplemente del core de Hilt para ViewModels asociados a la Activity/Fragment anfitrión.


package com.example.myapp.ui.compose

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
// Import para hiltViewModel
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.myapp.ui.MyViewModel // Tu ViewModel anotado con @HiltViewModel

@Composable
fun UserProfileScreen(
    // Inyecta el ViewModel usando la función Composable de Hilt
    // Hilt proveerá la instancia correcta asociada al scope adecuado (ej. Navigation graph)
    myViewModel: MyViewModel = hiltViewModel()
) {
    // Observa el StateFlow del ViewModel
    val userData by myViewModel.userDataFlow.collectAsStateWithLifecycle()

    // Llama a la función del ViewModel
    LaunchedEffect(key1 = Unit) {
        myViewModel.loadUserData()
    }

    Column {
        Text(text = "Perfil de Usuario (Compose con Hilt)")
        Text(text = userData)
        Text(text = "ViewModel instance hash: ${myViewModel.hashCode()}")
    }
}

// --- Para usar este Composable ---
// Asegúrate de que la Activity/Fragment que hostea Compose esté anotada con @AndroidEntryPoint.
// Luego, llama a UserProfileScreen() desde setContent o tu grafo de Navigation Compose.
// Ejemplo en una Activity:
/*
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.myapp.ui.compose.UserProfileScreen
import com.example.myapp.ui.theme.MyApplicationTheme
import dagger.hilt.android.AndroidEntryPoint // ¡Activity debe ser EntryPoint!

@AndroidEntryPoint
class ComposeActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                UserProfileScreen()
            }
        }
    }
}
*/

Nota: Inyectar dependencias directamente en funciones Composable (que no sean ViewModels) es posible pero menos común y requiere que el Composable se llame desde un @AndroidEntryPoint. La práctica recomendada es pasar dependencias a través del ViewModel.


Conclusión

Hilt simplifica enormemente la inyección de dependencias en Android al estandarizar la configuración y reducir el boilerplate asociado con Dagger. Su integración nativa con los componentes de Android y Jetpack, junto con la seguridad de la validación en tiempo de compilación, lo convierten en una opción robusta y eficiente para cualquier proyecto Android moderno.

Adoptar Hilt significa seguir las mejores prácticas recomendadas por Google y obtener una base sólida para construir aplicaciones mantenibles y testeables.

Para explorar más a fondo, consulta la documentación oficial de Hilt.

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