Dagger Hilt: Cómo hacer inyección de dependencias en Android
Antonio Leiva

Con la llegada de Android 11 también han llegado algunas novedades como Hilt, una librería de inyección de dependencias que ahora se convierte en la opción recomendada por Google.

Dagger Hilt ya está en versión estable, así que puedes empezar a usarlo en tus proyectos sin temor a que una actualización te rompa el código.

Hilt ¿Por qué ahora?

La comunidad Android lleva bastante tiempo un poco perdida sobre cómo implementar la inyección de dependencias.

Dagger ha sido siempre la alternativa elegida por la gran mayoría, pero es cierto que es una librería muy compleja y que la curva de aprendizaje es alta.

Además, existen muchas maneras de hacer lo mismo, y siempre ha habido muchas dudas sobre cuál era la mejor.

Es por ello que la comunidad Android lleva desde tiempo pidiendo a Google que se posicionara y nos diera una solución apta e integrada en el framework de Android.

Y esto es lo que tenemos con Hilt.

¿Pero por qué necesito Dagger Hilt en primer lugar?

Para eso nos tenemos que remontar a qué es un inyector de dependencias y qué problema soluciona.

Un inyector de dependencias es una entidad en tu código que provee las dependencias que otras entidades necesitan de forma activa.

Pero seguramente esto no te diga nada, así que vamos empezar por el principio.

El problema de las dependencias

Normalmente, cuando empiezas a preocuparte por la calidad de tu software, necesitas empezar a crear entidades y módulos que solamente hagan una cosa.

Este se conoce como el Principio de Responsabilidad Única de los Principios SOLID, y es de los principios más importantes en la programación: si una entidad solo hace una cosa, solo tendrá una razón para cambiar, y por tanto los efectos de que algo externo lo modifique se reducen.

Una unidad independiente es mucho más fácil de modificar, reemplazar y testear

Si quieres saber más sobre los Principios SOLID, puedes descargarte esta guía gratuita que he creado para ti.

Al hacer esto, tu módulo empezará a depender de muchos otros, y si los creamos dentro del propio módulo tendremos dos problemas principales

  1. Desde fuera del mismo, no somos capaces de ver con qué otros módulos interactúa. Tendríamos que entrar en el código fuente y estudiarlo a fondo, lo que es una pérdida de tiempo
  2. Durante los tests, no tendremos forma fácil de probar nuestro módulo de forma aislada, ya que al probar cualquiera de sus funcionalidades irremediablemente estaremos ejecutando parte de la funcionalidad de otro módulo

Así que la solución pasa por lo siguiente:

La Inversión de Control al rescate

Si en vez de que nuestro módulo o entidad cree sus propias instancias, se las proveemos nosotros mediante constructor, esto nos ayudará a solucionar ambos problemas:

  1. Las dependencias quedan explícitas en el constructor, así que no hay dudas de cuáles son los módulos de los que depende el nuestro.
  2. En los tests podemos proveer módulos alternativos que hagan que podamos probar nuestro componente de forma aislada.

Para que esto segundo sea cierto, podemos utilizar librerías como Mockito, o proveer nuestros propios dobles de test.

Esto segundo solo será posible si, además de la inversión de control, aplicamos la Inversión de Dependencias (otro Principio SOLID).

No los confundas, porque son dos conceptos muy similares: la Inversión de Dependencias nos dice que deberíamos depender de abstracciones, no concreciones. Es decir, que uses interfaces en vez de clases concretas.

La provisión de dependencias se vuelve una tarea compleja

Imagina que tienes en tu App un montón de módulos, y que todos ellos exponen sus dependencias y esperan que alguien se las provea.

Esto se vuelve de una complejidad enorme, y es importante hacerlo de forma estructurada y sencilla.

Hay muchos sistemas de provisión de dependencias, y hoy no voy a entrar en ellos (coméntame en los comentarios si quieres que otro día ahondemos), pero hoy vamos a centrarnos en la inyección de dependencias.

Dagger Hilt y la inyección de dependencias

Ya hemos entendido el problema, y que la solución es la inyección de dependencias. ¿Pero qué es la inyección de dependencias?

Muy a grandes rasgos es una forma automática de proveer las dependencias a un módulo.

Imagínate esto como un saco de piezas que conforman tu App. Cuando queremos crear una pieza nueva, el inyector se irá al saco, buscará las piezas que necesita, y las usará para crear esa nueva pieza, que a su vez se incluye también en el saco por si alguien más la necesita.

El objetivo de Dagger Hilt es hacer esto muy sencillo e integrado con el framework de Android.

Cómo usar Dagger Hilt

Vamos a ver los pasos de cómo se usaría Dagger Hilt en un proyecto que ahora mismo usa Dagger.

Para ello, mis cambios los haré a partir de esta etiqueta en mi repositorio de Architect Coders, que es la base de código que sustenta este programa de formación.

Configura el build.gradle de app

Se requieren las siguientes dependencias:

dependencies {
    implementation 'com.google.dagger:hilt-android:2.35.1'
    kapt 'com.google.dagger:hilt-compiler:2.35.1'
}

Además, necesitas configurar el kapt para que corrija los errores de tipos:

kapt {
    correctErrorTypes = true
}

Añade el plugin de Gradle de Hilt

Este es un paso opcional, pero que simplifica la forma de usar Hilt en Android, así que te recomiendo que lo hagas. En el build.gradle principal, añade el plugin de Hilt:

dependencies {
    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35.1'
}

Y después necesitas añadir el plugin en el build.gradle del módulo app:

plugins {
    id 'com.android.application'
    ...
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

Identifica cuál va a ser el Application

Para ello, lo único que tienes que hacer es usar la anotación @HiltAndroidApp:

@HiltAndroidApp
class MoviesApp : Application()

Si has usado Dagger anteriormente, recordarás que aquí se inicializaba en onCreate(). Con Hilt no hace falta, todo esto lo hace solo.

Crea módulos y añadelos a su Component correspondiente

Nuevamente, si has usado Dagger, recordarás que tienes módulos para los que luego tienes que crear un Component, y asignar el módulo al component correspondiente.

En Hilt se simplifica esto porque han incluido una serie de componentes con ciertos superpoderes especiales.

Si por ejemplo queremos tener dependencias a nivel de aplicación, usaríamos el SingletonComponent. Solo tenemos que declararlo con la anotación @InstallIn:

@Module
@InstallIn(SingletonComponent::class)
class AppModule

Luego ya solo tendríamos que definir las dependencias que queremos proveer con este módulo mediante la anotación @Provides. Si son dependencias que queremos que sean únicas para la App (en vez de que se genere una nueva cada vez que se pida), utilizaremos la anotación @Singleton:

    @Provides
    @Singleton
    fun databaseProvider(app: Application): MovieDatabase = Room.databaseBuilder(
        app,
        MovieDatabase::class.java,
        "movie-db"
    ).build()

Como ves, esta dependencia a su vez necesita el Application. ¿Y de dónde sale ese Application? De ahí los superpoderes.

Cada Component tiene una serie de dependencias a las que se puede acceder sin necesidad de hacer nada. Estos son cada uno de los componentes y sus dependencias:

ComponentBindings por defecto
SingletonComponentApplication
ViewModelComponentSavedStateHandle
ActivityComponentApplicationActivity
FragmentComponentApplicationActivityFragment
ViewComponentApplicationActivityViewew
ViewWithFragmentComponentApplicationActivityFragmentView
ServiceComponentApplicationService
Extraída de la referencia de Dagger Hilt

Definiendo dependencias a nivel de Activity

Hay veces que solo queremos que ciertas dependencias estén activas durante el tiempo de vida de un elemento del framework de Android. Como ves en el recuadro anterior, se pueden definir componentes para distintos elementos, pero aquí lo vamos a ver con una Activity.

Para ello, solo tenemos que usar el ActivityComponent:

@Module
@InstallIn(ActivityComponent::class)
class MainActivityModule {

    @Provides
    fun mainViewModelProvider(getPopularMovies: GetPopularMovies) = MainViewModel(getPopularMovies)

    @Provides
    fun getPopularMoviesProvider(moviesRepository: MoviesRepository) =
        GetPopularMovies(moviesRepository)
}

Declarar qué elementos pueden inyectar dependencias

Por defecto no podremos usar el módulo anterior en una Activity si no lo indicamos. Para ello, tendremos que usar la anotación @AndroidEntryPoint.

Hay una serie de elementos que se pueden marcar con esta anotación, que son:

  1. Activity
  2. Fragment
  3. View
  4. Service
  5. BroadcastReceiver

Para hacerlo con nuestra Activity, solo tendríamos que hacer:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var getPopularMovies: GetPopularMovies

    ...
}

Es importante tener claro que todo el código de inyección ocurre en el onCreate() de la clase correspondiente, así que no hacer nunca nada con las dependencias antes de llamar a super.onCreate()

¿Y qué pasa con el ViewModel?

Los ViewModels también utilizan a una anotación propia llamada @HiltViewModel.

@HiltViewModel
class MainViewModel @Inject constructor(...) : ViewModel() {
    ...
}

Con esto, ya podríamos acceder al ViewModel sin necesidad de crearnos una factory, tanto desde ViewModelProvider() como desde el delegado by viewModels() de fragment-ktx o activity-ktx.

For ejemplo, para activities:

implementation "androidx.activity:activity-ktx:1.2.3"

Y luego en la Activity:

private val viewModel: MainViewModel by viewModels()

También hay que cambiar el component al que se asigna el módulo de Dagger correspondiente, para que las dependencias sobrevivan durante todo el tiempo de vida del ViewModel, y no se recreen cuando haya un cambio de configuración (como una rotación de pantalla):

@Module
@InstallIn(ViewModelComponent::class)
class MainActivityModule

Dependencias en el scope de ViewModel

Además, si quieres que una dependencia se enganche al scope del ViewModel (es decir, que se cree una única instancia por cada instancia del ViewModel), puedes utilizar la anotación @ViewModelScoped:

@Provides
@ViewModelScoped
fun getPopularMoviesProvider(moviesRepository: MoviesRepository) =
    GetPopularMovies(moviesRepository)

Pasar argumentos al ViewModel desde la Activity o Fragment

Hay un caso un poco más complejo que es el de inyectar un argumento que viene de una Activity o Fragment a un ViewModel.

Un caso muy típico es, en una actividad de detalle, pasarle el id del elemento a cargar.

Si vuelves a la tabla que vimos anteriormente, la única dependencia inyectada automáticamente en el ViewModelScope es el SavedStateHandle, por tanto no tenemos acceso a la Activity para extraer esta información del intent.

¿Cómo lo hacemos? Pues realmente ya te he dado la pista que necesitas: usando el SavedStateHandle.

Como hemos visto, el SavedStateHandle se añade automáticamente a las dependencias del módulo, así que lo podemos usar como base para otras dependencias:

@Provides
@Named("movieId")
fun movieIdProvider(stateHandle: SavedStateHandle): Int = ...

¿Y ahora qué? Pues la magia no para.

Todos los extras del intent los tenemos , sin hacer nada, disponibles en el SavedStateHandle. Lo único que necesitas es recuperarlos:

@Provides
@Named("movieId")
fun movieIdProvider(stateHandle: SavedStateHandle): Int = 
   stateHandle.get<Int>(DetailActivity.MOVIE)

El único problema es que esta función puede devolver nulos, así que lo tendremos que gestionar de alguna forma.

En este caso estoy devolviendo una excepción indicando que ha ocurrido un erro inesperado:

@Provides
@Named("movieId")
fun movieIdProvider(stateHandle: SavedStateHandle): Int =
    stateHandle.get<Int>(DetailActivity.MOVIE)
        ?: throw IllegalStateException("Movie Id not found in the state handle")

Y con todo esto, ya tendrías el ejemplo funcionando.

Puedes ver el código en esta rama del repositorio de Architect Coders.

Dagger Hilt es un claro paso adelante en simplicidad

Hilt mejora muchas de las complejidades que suponía empezar a trabajar con inyección de dependencias en Android, y hace este concepto mucho más accesible.

Por otro lado, sigue estando a un paso de la simplicidad de otras soluciones como Koin, y por tanto la curva de aprendizaje sigue siendo un poco mayor.

Quizá también te interese…

Text en Jetpack Compose: da vida a tus textos

Text en Jetpack Compose: da vida a tus textos

Los textos son una parte imprescindible en cualquier interfaz de usuario, y por tanto es importante saber cómo usarlos y sacarles el máximo partido. https://youtu.be/yu6rxgBEh1Y En Jetpack Compose, el Composable encargado de renderizar texto se llama simplemente Text...

Modifiers: Personaliza cualquier vista en Jetpack Compose

Modifiers: Personaliza cualquier vista en Jetpack Compose

Muchas veces no nos es suficiente con la configuración básica que nos provee una vista, y por tanto vamos a necesitar modificarla para adaptarla a nuestras necesidades. Esto es exactamente para lo que sirven los Modifiers. Es un cajón de sastre que nos da la opción de...

Layouts en Jetpack Compose: Estructura la UI con Box, Column y Row

Layouts en Jetpack Compose: Estructura la UI con Box, Column y Row

Organizar los elementos de UI en la pantalla siempre es una parte importante, y tenemos los layouts en Jetpack Compose que nos van a permitir hacerlo de distintas formas en función de nuestras necesidades. https://youtu.be/xyBkLS5OPtk Si te fijas, en el artículo...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *