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

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