Noticia Cómo implementar Inyección de Dependencias de forma sencilla usando Hilt

Cómo implementar Inyección de Dependencias de forma sencilla usando Hilt


Si alguna vez has sentido que tu código de Android se está volviendo un caos de instanciaciones manuales, no estás solo. Gestionar las dependencias a mano es un auténtico dolor de cabeza, ya que te obliga a construir cada clase y sus requisitos uno por uno, usando contenedores que acaban siendo laberintos difíciles de mantener. Aquí es donde entra en juego Hilt, una joya de biblioteca diseñada específicamente para quitarte de encima todo ese trabajo repetitivo y aburrido.

Básicamente, Hilt se monta sobre los hombros de Dagger, aprovechando su potencia y seguridad en tiempo de compilación, pero envolviéndolo todo en una capa mucho más amigable para los desarrolladores de Android. En lugar de pelearte con la configuración compleja de Dagger, Hilt te ofrece estándares ya definidos y contenedores automáticos que se adaptan a los ciclos de vida de tu app, permitiéndote centrarte en lo que de verdad importa: hacer que tu aplicación funcione de maravilla.

Manos a la obra: Configuración inicial​


Para empezar a usar Hilt, lo primero es dejar el terreno listo en Gradle. No te olvides de añadir el complemento hilt-android-gradle-plugin en el archivo build.gradle raíz de tu proyecto. Una vez hecho esto, debes aplicar el plugin en el archivo de tu módulo app y añadir las dependencias necesarias, incluyendo el compilador de Hilt. Un detalle fundamental es que, para que todo ruede sin problemas con Jetpack Compose, tu proyecto debe estar configurado para Java 17, algo que debes especificar claramente en tu archivo de configuración de la app.

El corazón de la app: La clase Application​


Cualquier proyecto que quiera aprovechar Hilt debe tener una clase Application personalizada. El truco está en añadirle la anotación @HiltAndroidApp. Esta pequeña línea de código es la que activa toda la magia de la generación de código de Hilt, creando una clase base que actúa como el contenedor de dependencias maestro a nivel de aplicación. Al estar ligada al ciclo de vida del objeto Application, este componente es la raíz de todo; cualquier otro componente de la app podrá acceder a las dependencias que se definan aquí.

Cómo inyectar dependencias en los componentes de Android​


Una vez que la clase Application está lista, puedes empezar a meter mano en el resto de las clases. Para que Hilt sepa dónde debe suministrar dependencias, utilizamos la anotación @AndroidEntryPoint. Esta anotación es compatible con una gran variedad de clases, como Activities, Fragments, Views, Services y BroadcastReceivers. En el caso de Jetpack Compose, no hace falta anotar cada función componible, basta con que la Activity raíz tenga el @AndroidEntryPoint para que toda la jerarquía de la interfaz de usuario pueda acceder a los ViewModels inyectados.

Para pedirle a Hilt que nos dé una instancia concreta de algo, usamos la anotación @Inject para realizar la inyección de campo. Eso sí, ten mucho cuidado con un detalle: los campos inyectados no pueden ser privados, ya que si intentas hacer esto, el compilador te lanzará un error y no podrás generar la app.

Definiendo cómo se crean las dependencias​


Hilt necesita instrucciones claras sobre cómo proporcionar las instancias que solicitamos. La forma más directa es la inyección de constructor. Al añadir @Inject en el constructor de una clase, le estás diciendo a Hilt: «Mira, así es como se crea esta clase y estas son las piezas que necesita». Durante la compilación, Hilt y Dagger validan que no haya ciclos de dependencia ni piezas faltantes, asegurando que todo encaje perfectamente antes de que la app llegue al dispositivo.

Uso de Módulos: Cuando el constructor no es suficiente​


Habrá ocasiones en las que no puedas usar la inyección de constructor, por ejemplo, cuando trabajas con interfaces o clases de librerías externas como Retrofit u OkHttpClient. Para solucionar esto, recurrimos a los módulos de Hilt, que son clases anotadas con @Module. Estos módulos deben ir acompañados de @InstallIn para indicar en qué componente de Android estarán disponibles las dependencias.

  • Uso de @Binds: Se emplea principalmente para interfaces. Creamos una función abstracta que le indica a Hilt qué implementación concreta debe usar cuando alguien pida una interfaz específica.
  • Uso de @Provides: Es la solución ideal para clases que no son nuestras o que requieren una configuración compleja (como el patrón Builder). Aquí escribimos el cuerpo de la función para instanciar el objeto manualmente y devolverlo.

Gestionando múltiples implementaciones con Calificadores​


A veces necesitamos dos versiones del mismo tipo de objeto, como dos clientes de OkHttpClient con diferentes interceptores. Para evitar que Hilt se confunda, 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. Es una regla de oro aplicar el calificador en todas las rutas de suministro para evitar errores inesperados.

Además, Hilt ya nos regala algunos calificadores muy útiles para no tener que crearlos desde cero, como @ApplicationContext y @ActivityContext, que nos permiten inyectar el contexto de la aplicación o de la actividad de forma sencilla y segura.

Componentes, Ciclos de Vida y Alcances​


Cómo implementar Inyección de Dependencias de forma sencilla usando Hilt


Hilt genera automáticamente componentes que viven y mueren junto con las clases de Android. Por ejemplo, el SingletonComponent vive tanto como la aplicación, mientras que el ActivityComponent se destruye al morir la actividad. Esto es vital para gestionar la memoria de la app.

Por defecto, Hilt crea una instancia nueva cada vez que se pide una dependencia. Si queremos que se mantenga la misma instancia durante todo el ciclo de vida de un componente, usamos anotaciones de alcance como @Singleton, @ActivityScoped o @ViewModelScoped. Eso sí, no abuses de esto, ya que mantener objetos en memoria puede elevar el consumo de recursos si no es estrictamente necesario.

Casos especiales: @AssistedInject y Puntos de Entrada​


Cuando necesitamos inyectar valores que solo conocemos en tiempo de ejecución (como un ID que viene en un Intent), utilizamos @AssistedInject. Esto nos permite combinar dependencias gestionadas por Hilt con parámetros proporcionados manualmente a través de una Factory. Por otro lado, si necesitamos inyectar dependencias en clases que Hilt no soporta nativamente, como los ContentProviders, creamos un @EntryPoint. Este actúa como un puente que permite al código no administrado acceder al grafo de dependencias de Hilt mediante EntryPointAccessors.

Hilt frente a Dagger​


Aunque Hilt se basa en Dagger, su objetivo es eliminar la verbosidad. Mientras que en Dagger tendrías que escribir manualmente los componentes y gestionar la infraestructura, Hilt automatiza la creación de componentes y ofrece vinculaciones predefinidas. Ambos pueden convivir en el mismo proyecto, pero lo ideal es dejar que Hilt tome el mando para lograr un código más limpio, legible y fácil de testear.

Al implementar esta arquitectura, conseguimos que nuestra aplicación sea modular, fácil de mantener y extremadamente flexible para realizar pruebas unitarias. El uso de contenedores automatizados, la gestión inteligente de los alcances y la capacidad de integrar dependencias externas convierten a esta herramienta en la opción predilecta para cualquier desarrollo moderno en Android.

Continúar leyendo...