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
- 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
- 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:
- 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.
- 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:
Component | Bindings por defecto |
---|---|
SingletonComponent | Application |
ViewModelComponent | SavedStateHandle |
ActivityComponent |
|
FragmentComponent |
|
ViewComponent |
|
ViewWithFragmentComponent |
|
ServiceComponent |
|
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:
- Activity
- Fragment
- View
- Service
- 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 asuper.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.
0 comentarios