Si alguna vez has sentido que tu proyecto de Android se convierte en un auténtico laberinto de instanciaciones manuales, no te preocupes, que es más común de lo que parece. Gestionar cada objeto y sus requisitos uno a uno es un dolor de cabeza monumental que suele derivar en un código difícil de mantener y propenso a errores. Para solucionar este caos, Hilt aparece como una solución brillante que nos quita de encima todo ese trabajo repetitivo, permitiéndonos centrar la energía en lo que realmente importa: que la app funcione de maravilla.
Básicamente, Hilt se apoya en la potencia de Dagger, aprovechando su seguridad en tiempo de compilación y su rendimiento, pero envolviéndolo en una capa mucho más digerible para nosotros los desarrolladores. En lugar de pelearnos con configuraciones infinitas, Hilt nos ofrece un estándar ya definido con contenedores automáticos que se sincronizan perfectamente con los ciclos de vida de Android, facilitando enormemente la creación de aplicaciones modulares y fáciles de testear.
Configuración y el núcleo de la aplicación
Para poner en marcha Hilt, lo primero es dejar el terreno listo en Gradle. No podemos olvidar añadir el complemento hilt-android-gradle-plugin en el archivo build.gradle raíz. Después, aplicamos el plugin en el módulo de la app y sumamos las dependencias necesarias, incluyendo el compilador. Un punto crítico es que, para que todo ruede sin problemas con Jetpack Compose, el proyecto debe estar configurado para Java 17, detallándolo en la configuración de la aplicación.
Cualquier app que quiera aprovechar esta tecnología debe contar con una clase Application personalizada. El truco está en añadir la anotación @HiltAndroidApp, que es la que activa toda la maquinaria de generación de código. Esta línea crea una clase base que actúa como el contenedor maestro de dependencias a nivel de aplicación, sirviendo como raíz de la cual podrán beber todos los demás componentes del sistema.
Inyectando dependencias en componentes Android
Una vez que el núcleo está listo, podemos empezar a suministrar dependencias en el resto de las clases. Para que Hilt sepa dónde debe intervenir, utilizamos la anotación @AndroidEntryPoint. Esta es compatible con una gran variedad de componentes como Activities, Fragments, Views, Services y BroadcastReceivers. En el ecosistema de Jetpack Compose, no hace falta anotar cada función componible; basta con que la Activity raíz tenga la anotación para que toda la jerarquía de la interfaz de usuario acceda a los ViewModels inyectados.
Cuando queremos que Hilt nos entregue una instancia concreta, recurrimos a la anotación @Inject para realizar la inyección de campo. Aquí hay un detalle vital: los campos inyectados no pueden ser privados. Si intentas hacerlo, el compilador te dará un error y la app no se generará. Por otro lado, la forma más directa de definir cómo se crea una clase es la inyección de constructor, añadiendo @Inject directamente en el constructor de la clase para indicarle a Hilt qué piezas necesita para existir.
El uso de Módulos: @Binds y @Provides
Habrá momentos en los que la inyección de constructor no sea suficiente, sobre todo cuando trabajamos con interfaces o clases de librerías externas como Retrofit u OkHttpClient. Para estos casos, usamos los módulos de Hilt, que son clases anotadas con @Module y deben llevar @InstallIn para definir en qué componente de Android estarán disponibles las dependencias.
- Uso de @Binds: Se emplea principalmente para interfaces. Mediante una función abstracta, le indicamos a Hilt qué implementación concreta debe usar cuando alguien solicite una interfaz específica.
- Uso de @Provides: Es la solución ideal para clases que no son nuestras o que requieren configuraciones complejas, como el patrón Builder. Aquí escribimos el cuerpo de la función para instanciar el objeto manualmente y devolverlo.
Gestión de múltiples implementaciones con @Qualifier
A veces necesitamos dos versiones del mismo tipo de objeto, por ejemplo, dos clientes de OkHttpClient con distintos interceptores. Para evitar que Hilt se confunda y lance un error de ambigüedad, creamos calificadores personalizados mediante anotaciones propias marcadas con @Qualifier. De este modo, podemos etiquetar cada proveedor y pedir la versión exacta que necesitamos en el punto de inyección.
Como norma de oro, si añades un calificador a un tipo, debes aplicarlo en todas las rutas de suministro para evitar que Hilt inserte una dependencia incorrecta. Afortunadamente, Hilt ya nos regala algunos calificadores muy útiles, como @ApplicationContext y @ActivityContext, que nos permiten inyectar el contexto de la aplicación o de la actividad de forma sencilla y segura.
Componentes, Alcances y Ciclos de Vida
Hilt genera componentes que viven y mueren junto con las clases de Android. El SingletonComponent, por ejemplo, persiste mientras la aplicación esté viva, mientras que el ActivityComponent se destruye al morir la actividad. Por defecto, Hilt crea una instancia nueva cada vez que se pide una dependencia, pero si queremos mantener la misma instancia, usamos anotaciones de alcance como @Singleton, @ActivityScoped o @ViewModelScoped.
Es importante no abusar de estos alcances, ya que mantener objetos en memoria puede elevar el consumo de recursos. Lo ideal es usarlos solo en objetos que tengan un estado interno que deba persistir o que sean extremadamente costosos de crear. La jerarquía de componentes permite que un módulo instalado en un componente superior pueda ser accedido por sus componentes secundarios, optimizando así la distribución de dependencias.
Casos especiales: EntryPoints y @AssistedInject
Cuando necesitamos inyectar valores que solo conocemos en tiempo de ejecución, como un ID que llega en un Intent, utilizamos @AssistedInject. Esto nos permite mezclar dependencias gestionadas por Hilt con parámetros proporcionados manualmente. Por otro lado, si necesitamos inyectar dependencias en clases que Hilt no soporta nativamente, como los ContentProviders, creamos un @EntryPoint.
El punto de entrada actúa como un puente entre el código gestionado por Hilt y el que no lo está. Para acceder a él, utilizamos el método estático EntryPointAccessors, pasando la instancia del componente o el objeto @AndroidEntryPoint correspondiente. Esto garantiza que incluso las partes más oscuras del framework de Android puedan acceder al grafo de dependencias de forma eficiente.
La implementación de este sistema permite que el código sea mucho más limpio y flexible que el Dagger puro, eliminando la verbosidad y automatizando la creación de componentes. Al combinar el uso inteligente de calificadores para evitar colisiones, el aprovechamiento de @Binds para el desacoplamiento de interfaces y una gestión estricta de los alcances para cuidar la memoria, conseguimos una arquitectura robusta, escalable y profesional en cualquier proyecto moderno de Android. Comparte la información para que otros usuarios conozcan del tema.
Continúar leyendo...