Cómo integrar Hilt en un proyecto Android modular

,

En este post mostraremos cómo integrar la librería de Hilt en un proyecto Android modular. Por tanto, es recomendable tener un conocimiento básico sobre los conceptos que se manejan con esta librería, por lo que te recomiendo leer este artículo antes de continuar.

¿Por qué Hilt?

Hilt es una nueva librería de inyección de dependencias -DI de ahora en adelante- que está basada en Dagger, y que desde hace poco tiempo pasó a estar en fase Beta.

Al estar basada en Dagger, mantiene los mismos beneficios de esta librería pero a la vez intenta simplificar algunos de los aspectos más tediosos de su implementación en nuestros proyectos.

Dagger suele ser la elección más común a la hora de usar una librería de DI a pesar de su pronunciada curva de aprendizaje. Debido a esto, existen alternativas más simples como Koin que, a su vez, presenta alguna desventaja importante como la falta de detección de errores en tiempo de compilación.

En este contexto surge Hilt, con el objetivo de ofrecer una forma normalizada de implementar Dagger con las siguientes características:

  • Facilitar la integración en los proyectos Android
  • Simplificar su configuración, ofreciendo componentes estándar para las diferentes clases Android
  • Reducir el código boilerplate que genera Dagger

Añadir dependencias de Hilt

El primer paso será añadir el plugin y las dependencias necesarias a nuestro proyecto.

Para ello, debemos añadir las siguientes dependencias en el archivo build.gradle de nuestros módulos Android:

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

kapt {
 correctErrorTypes true
}

Debemos especificarle al kapt que corrija los errores de tipo poniendo la sentencia correctErrorTypes  a true.

Aplicar el plugin de Hilt

A continuación, añadimos el plugin a nuestro fichero build.gradle del proyecto y lo aplicamos en los módulos Android.

buildscript {
  repositories {
    google()
    jcenter()
  }
  dependencies {
    // ...
    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.33-beta'
  }
}
// ...
apply plugin: 'dagger.hilt.android.plugin'

android {
  // ...
}

El plugin de Hilt simplifica el uso de la anotación @AndroidEntryPoint (que usaremos para inyectar dependencias en las clases Android) y @HiltAndroidApp (que utilizaremos para anotar nuestra clase Application), evitando que tengamos que hacer referencias a las clases autogeneradas por Hilt de forma manual.

Inicialización de Hilt

Para que Hilt comience la generación de código de los componentes, debemos anotar nuestra clase Application con @HiltAndroidApp. A diferencia de Dagger, en el que era necesario inicializarlo en el método onCreate() de esta clase, Hilt realizará su propia inicialización a través de esta anotación.

@HiltAndroidApp
class App : Application() {
  // …
}

Llegados a este punto, simplemente hemos explicado cómo instalar Hilt en nuestro proyecto. En las siguientes cuestiones vamos a ver cómo implementarlo en un proyecto modular.

Contexto de la arquitectura

Antes de entrar en materia, vamos a explicar el esquema simplificado de la arquitectura con la que vamos a trabajar.

En primer lugar, tenemos el módulo App de tipo Android, que será el módulo principal de la aplicación y el punto de entrada para Hilt. Este módulo debe tener visibilidad del resto de módulos del proyecto. Como hemos comentado anteriormente, contendrá nuestra clase Application con la anotación @HiltAndroidApp.

Las funcionalidades de la app se encapsularán en módulos Feature, también de tipo Android. En estos módulos se implementarán las vistas y los casos de uso.

Finalmente, tendremos una última capa para los datos, en la que encapsularemos los módulos de Repository junto con las fuentes de datos Remote y Local. Los módulos Repository Remote tienen una diferencia importante respecto al resto porque serán módulos puro Kotlin, esto quiere decir que no utilizarán ninguna dependencia de Android en ellos.

Inyección en clases Android

Para que Hilt pueda proporcionar dependencias a nuestras clases Android, debemos usar la anotación @AndroidEntryPoint. Las clases Android soportadas por esta anotación son las siguientes:

  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver
@AndroidEntryPoint
class HomeFragment : BaseFragment() {
  // …
}

Es importante destacar que si anotamos una clase con @AndroidEntryPoint, las clases que dependan de ella también deberán ser anotadas. Por ejemplo, la actividad en la que se usa ese Fragment.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  // ...
}

Inyección de ViewModel

En primer lugar, empezaremos por las interfaces de los módulos  Feature . A partir de la versión 2.31 de Hilt, se ha añadido la anotación  @HiltViewModel  para facilitar la inyección por constructor de un  ViewModel  en la vista.

@HiltViewModel
class HomeViewModel @Inject constructor(
   private val getAllNewsUseCase: GetAllNewsUseCase
) : BaseViewModel() {
  // ...
}

Como ves, el constructor del ViewModel también debe ser anotado con @Inject.

Para inyectar el ViewModel en el Fragment, podemos usar el tradicional ViewModelProvider o la función de extensión by viewModels().

@AndroidEntryPoint
class HomeFragment : BaseFragment() {

   private val viewModel: HomeViewModel by viewModels()

}

Además, en nuestro caso usamos la librería Navigation de Jetpack, por lo que nos puede interesar asociar el  ViewModel  con alcance al grafo de navegación. Para ello, será necesario añadir la siguiente dependencia a nuestro proyecto y usar la función  hiltNavGraphViewModels()

dependencies {
  ...
  implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0-beta01'
}

// Ejemplo con alcance al grafo de navegación
@AndroidEntryPoint
class HomeFragment : BaseFragment() {

   private val viewModel: HomeViewModel by hiltNavGraphViewModels(R.id.nav_graph__home_feature)

}

Para simplificar el uso de estas dos funciones hemos implementado el siguiente método que, en función del parámetro de entrada, utiliza una u otra opción.

inline fun  viewModelBinder(@IdRes navGraphId: Int? = null): Lazy =
   when (navGraphId) {
       null -> viewModels()
       else -> hiltNavGraphViewModels(navGraphId)
   }


// Ejemplo de uso con alcance al grafo de navegación
@AndroidEntryPoint
class HomeFragment : BaseFragment() {

   private val viewModel: HomeViewModel by viewModelBinder(R.id.nav_graph__home_feature)

}


// Ejemplo de uso con alcance al Fragment
@AndroidEntryPoint
class HomeFragment : BaseFragment() {

   private val viewModel: HomeViewModel by viewModelBinder()

}

Para terminar, si te fijas en el constructor del ViewModel que hemos inyectado, recibe un objeto por parámetro, el cual a su vez estamos inyectando en el ViewModel a través de un módulo de Hilt.

Los módulos en Hilt son similares a los de Dagger, pero tienen algunas diferencias a la hora de implementarlos. Cuando creamos un módulo en Dagger hay que asociarlo a un Component que hemos creado previamente. Sin embargo, Hilt dispone de unos componentes por defecto en los que podremos instalar nuestros módulos en función del contexto en el que queramos inyectar las dependencias. Los componentes en los que podemos instalar nuestros módulos son los siguientes:

SingletonComponentApplication
ActivityRetainedComponentN/A
ServiceComponentService
ActivityComponentActivity
ViewModelComponentViewModel
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponentView con anotación @WithFragmentBindings

Para ampliar la información sobre los componentes disponibles en Hilt, sus ciclos de vida, alcances, etc., se puede consultar la documentación oficial de Android.

En nuestro caso, como estamos inyectando en un ViewModel, instalaremos en el componente ViewModelComponent el módulo que queremos proveer, usando la anotación @InstallIn.

@Module
@InstallIn(ViewModelComponent::class)
object HomeProvideModule {

   @Provides
   fun getAllNewsUseCaseProvider(repository: NewsRepository) =
       GetAllNewsUseCaseImpl(repository) as GetAllNewsUseCase

}

Adicionalmente, si quisiéramos proveer una dependencia con alcance a nuestro ViewModel, Hilt nos facilita esta tarea con la anotación @ViewModelScoped. Esta anotación hará que la dependencia que proveemos a un ViewModel sea siempre la misma instancia para cualquier enlace que solicite esa dependencia en ese ViewModel.

Es importante entender que la instancia de la dependencia que se provee con esta anotación no es la misma para diferentes ViewModels, ya que Hilt crea una instancia del componente ViewModelComponent para cada ViewModel.

Hilt en módulos puro Kotlin

Hasta el momento, hemos hablado de Hilt aplicado a módulos Android, pero ¿qué ocurre con los módulos que son puro Kotlin como el módulo repositorio y el remoto? Para estos módulos debemos añadir un conjunto de dependencias diseñadas específicamente para módulos puros, que contienen la funcionalidad core de Hilt.

dependencies {
  implementation "com.google.dagger:hilt-core:2.33-beta"
  kapt "com.google.dagger:hilt-compiler:2.33-beta"
}

Hay que tener en cuenta que este core no está asociado al framework de Android, y por lo tanto solo podemos instalar nuestros módulos sobre el componente SingletonComponent que nos provee dicho core.

@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {

   @Provides
   @Singleton
   fun newsRepositoryProvider(
       newsRemoteDataSource: NewsRemoteDataSource,
       newsLocalDataSource: NewsLocalDataSource
   ) = NewsRepositoryImpl(newsRemoteDataSource, newsLocalDataSource) as NewsRepository

}

De forma análoga para el módulo Remote, creamos otro módulo que se instale en SingletonComponent y provea sus dependencias.

@Module
@InstallIn(SingletonComponent::class)
class RemoteModule {
  // ...
}

Conclusiones

Hilt cumple con el objetivo de simplificar la integración/configuración de su predecesor Dagger. Aún así, y aunque es cierto que mantiene conceptos y enfoques similares, la curva de aprendizaje sigue siendo un poco alta.

Otro de los aspectos positivos a destacar de Hilt, es la posibilidad de poder usarlo en módulos Kotlin sin necesidad de que estos módulos tengan que depender del framework de Android.

En conclusión, aunque sea una librería que está en fase Beta, tiene muy buena pinta y se postula como el heredero natural de Dagger, por lo que tendremos que estar atentos para ver cómo evoluciona en las próximas versiones.







Fernando Galiay,
Arquitecto Android