Si alguna vez has sentido que el código de tu aplicación de Android se vuelve un auténtico caos, con instancias que pasas de una clase a otra como si fueran una patata caliente, es muy probable que hayas caído en el infierno de las dependencias. Para evitar que tu proyecto se vuelva inmanejable, Google ha puesto en nuestro camino a Dagger Hilt, una herramienta que se encarga de hacer todo el trabajo pesado, automatizando la entrega de los objetos que tus clases necesitan sin que tengas que escribir el mismo código una y otra vez hasta hartarte.
A diferencia del Dagger convencional, que a veces puede asustar por su curva de aprendizaje tan pronunciada, Hilt propone un camino mucho más estandarizado y directo. Se lleva de maravilla con Jetpack Compose y las arquitecturas modernas, optimizando el rendimiento en tiempo de ejecución y asegurando que no te lleves sorpresas desagradables con errores de dependencias faltantes, ya que todo se valida durante la compilación.
Configuración inicial y el arranque del sistema
Para empezar a usar Hilt, lo primero es dejar listo el entorno en los archivos de Gradle. Es crucial añadir el plugin de hilt-android-gradle-plugin en el archivo raíz y luego aplicar las dependencias en el módulo de la aplicación. Un punto clave aquí es que el proyecto debe estar configurado para Java 17, porque si no, las versiones actuales de Hilt y Compose podrían darte bastantes dolores de cabeza.
Para que la magia empiece a funcionar, cualquier app que quiera usar este sistema debe tener una clase Application marcada con la anotación @HiltAndroidApp. Esta etiqueta es el interruptor que activa la generación de código y crea la base donde residirá todo el grafo de objetos; sin ella, Hilt no tendría dónde anclarse y nada de lo que hagas funcionaría.
Inyectando dependencias en los componentes de Android
Una vez montada la base, debemos indicar a Hilt en qué clases queremos recibir los objetos. Para ello utilizamos @AndroidEntryPoint, que es compatible con la mayoría de las piezas del framework, como las Activities, Services y BroadcastReceivers. Si estás usando Compose, basta con anotar la Activity raíz y así todos los elementos composables podrán acceder a los ViewModels inyectados sin líos.
Para meter un objeto directamente en el código de una actividad, usamos la inyección de campo con @Inject. Solo tienes que definir la variable como lateinit var y añadir la anotación. Eso sí, ten mucho cuidado: los campos inyectados no pueden ser privados, ya que Hilt necesita acceso a ellos para asignar la instancia; si intentas hacerlos privados, el compilador te dará un error inmediato.
El arte de las vinculaciones: @Binds frente a @Provides
Hilt necesita saber cómo fabricar cada cosa que le pedimos. Lo más sencillo es la inyección de constructor, donde anotamos el constructor de la clase con @Inject. Pero hay casos donde esto es imposible, como cuando trabajamos con interfaces o librerías externas tipo Retrofit o Room, donde no podemos tocar el código fuente para añadir anotaciones.
Aquí es donde entran los módulos, que son clases anotadas con @Module y @InstallIn. Si queremos vincular una interfaz con una implementación concreta, lo más eficiente es usar @Binds. Se define una función abstracta donde el parámetro es la implementación y el retorno la interfaz; es básicamente decirle a Hilt: «cuando alguien quiera un AnalyticsService, dale un AnalyticsServiceImpl».
Por otro lado, cuando la creación del objeto requiere una lógica más compleja o viene de un tercero, usamos @Provides. Aquí escribimos una función normal donde el cuerpo detalla exactamente cómo se construye la instancia, como ocurre al configurar la URL base de un cliente de Retrofit. En resumen, mientras que @Binds sirve para delegar, @Provides se usa para fabricar la dependencia manualmente.
Gestión de conflictos mediante @Qualifier
A veces necesitamos dos versiones distintas de un mismo tipo de objeto. Imagina que tienes dos clientes de OkHttpClient: uno para llamadas que requieren autenticación y otro para peticiones públicas. Si intentamos inyectar ambos sin más, Hilt entrará en pánico porque no sabrá cuál elegir. Para solucionar este embrollo, recurrimos a los calificadores personalizados.
Un calificador es básicamente una anotación propia marcada con @Qualifier. Creamos etiquetas como @AuthInterceptorOkHttpClient y las ponemos tanto en el método del módulo que provee la instancia como en el lugar donde la inyectamos. De esta forma, Hilt puede diferenciar entre vinculaciones del mismo tipo sin errores. Además, existen calificadores ya creados muy útiles como @ApplicationContext y @ActivityContext, que nos ahorran crear módulos propios para obtener el contexto de Android.
Sincronización de ciclos de vida y Scoping
Por defecto, Hilt crea una instancia nueva cada vez que se pide una dependencia. Pero hay situaciones donde queremos que el objeto sea el mismo mientras viva el componente. Para esto existen los alcances o scopes. Si marcamos una clase con @Singleton y la instalamos en SingletonComponent, tendremos una única instancia compartida en toda la aplicación.
También hay niveles más específicos. Por ejemplo, @ActivityScoped asegura que se use la misma instancia durante toda la vida de una actividad, mientras que @ViewModelScoped hace lo propio en el ViewModel, evitando que la dependencia se recree al girar la pantalla. Eso sí, no conviene abusar de los scopes, ya que mantener objetos en memoria demasiado tiempo puede perjudicar el rendimiento y la gestión de memoria de Android.
Casos especiales: @EntryPoint y clases no soportadas
Hilt es muy potente, pero no llega a todas partes. Hay clases del sistema, como los ContentProvider, que no admiten @AndroidEntryPoint. En estos escenarios, la solución es crear un punto de entrada mediante @EntryPoint. Esto define una interfaz que sirve de puente entre el código gestionado por Hilt y el código externo.
Para recuperar las dependencias desde este puente, utilizamos los EntryPointAccessors. Le pasamos el contexto adecuado y la interfaz del EntryPoint, lo que nos permite extraer manualmente las dependencias del grafo de Hilt incluso en aquellos rincones donde la inyección automática no puede llegar.
Diferencias entre Hilt y Dagger
Hilt no es un sustituto de Dagger, sino que está construido sobre él. Su gran ventaja es que nos quita de encima la necesidad de escribir manualmente los componentes y de gestionar sus ciclos de vida, ya que genera automáticamente los contenedores vinculados a las clases de Android. Esto reduce una barbaridad el código repetitivo y hace que el proyecto sea mucho más legible.
Este sistema permite que la inversión de control sea algo natural: las clases dicen qué necesitan y Hilt se encarga de suministrarlas. Esto no solo facilita el día a día del desarrollo, sino que es una bendición para los tests unitarios, ya que podemos cambiar las implementaciones reales por dobles de prueba sin tener que tocar la lógica de negocio.
La implementación de Hilt en Android permite una gestión de dependencias robusta y eficiente, eliminando el código redundante y facilitando la creación de aplicaciones escalables. Gracias al uso preciso de @Binds, @Provides y @Qualifier, los desarrolladores pueden resolver conflictos de tipos y gestionar el ciclo de vida de los objetos de manera granular, asegurando que la aplicación sea mantenible y fácil de testear mediante la inversión de control.