Cómo usar Dagger Hilt en Android con ejemplos🥰- Kotlin

Roberto Fuentes
9 min readAug 11, 2020

--

Actualización: Recordad que este post se hizo en 2020, sigue siendo útil pero está desactualizado y sin ser mantenido. A mi parecer los diagramas/dibujos siguen siendo útiles y se pueden aprovechar pero siempre buscad posts más actualizados. Y por último, la práctica hace más que la lectura, tenedlo en cuenta a la hora de leer este post!

Foto de Jimmy Chang en Unsplash

Inyección de Dependencias (o en inglés DI, Dependency Injection), es una amplia técnica usada en el mundo de la programación. Donde las dependencias son proveídas a una clase en vez de crearlas por nosotros mismos.

¿Qué es Dagger Hilt?

Es un framework de inyección de dependencias que realiza operaciones en tiempo de compilación para Android, el cual se encarga de crear y administrar la creación de objetos en toda la aplicación.

¿Por qué usar Dagger Hilt?

Implementar Hilt en nuestra aplicación nos aporta una serie de beneficios:

  • Código reciclable
  • Reducción de código
  • Dependencias independientes.
  • Configuración simplificada
  • Fácil uso en pruebas (tests)
  • Componentes estandarizados

La teoría está genial pero… ¿cómo lo aplico en mi app?

Entramos en acción

Configuración

Primero agregaremos una línea de código en el build.gradle(Project) raíz.

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

Luego tendremos que añadir cuatro líneas de código en el apartado plugin y dependencies de nuestro módulo de Gradle build.gradle(Module:app)

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28.3-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28.3-alpha"
}

Activar Hilt

Para activar Hilt primero tenemos que saber lo siguiente:
Todas las apps que usan Hilt deben contener una clase Application anotada con@HiltAndroidApp

@HiltAndroidApp
class ExampleApplication : Application() {}

El componente @HiltAndroidApp activa la generación de código Hilt y se convierte en un contenedor de dependencias.

@HiltAndroidApp Contenedor de dependencias

Crear un componente

Una vez tenemos el contenedor ya podemos aportar dependencias a clases Android. Vamos a crear una activity llamada MainActivity y la vamos a anotar con @AndroidEntryPoint

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

Importante: Hilt soporta distintas clases de Android (Application, Activity, Fragment, View, Service y BroadcastReciver).
Si anotas una clase Android con @AndroidEntryPoint también debes anotar sus dependencias, ejemplo sencillo: Si tienes un fragment entonces también deberás anotar esa actividad ya que fragment depende de actividad, ambas clases terminan anotadas.

Tras esta breve pausa, seguimos con MainActivity. La anotación genera un componente Hilt por cada clase Android. Los componentes pueden recibir dependencias de sus respectivas clases padres.
En nuestro caso se nos generará solo un componente y podremos recibir dependencias de Activity o Application, pero no podremos recibir dependencias orientadas componentes que se encuentren por debajo, por ejemplo Fragment

Jerarquía componentes. Fuente: developer.android.com/hilt-android
Contenedor con un componente @AndroidEntryPoint

Inyectar las dependencias

Inyección de Campo (Field Injection)

A partir de ahora vamos a empezar a crear una idea super sencilla para entender como funciona Hilt.
Todos sabemos que para que un ordenador pueda funcionar necesita un cable que esté conectado a una corriente electrica, es decir, el ordenador depende de un cable y este de la corriente eléctrica.

Vamos a crear la clase Ordenador, el cual añadiremos un @Inject antes del constructor para decirle a Hilt como proporcionar instancias de esa clase (Ordenador) cuando alguien lo pida.

class Ordenador @Inject constructor(){
fun tengoOrdenador(): String{
return "Tengo una clase Ordenador"
}
}

Para poder pedir las dependencias desde un componente tendremos que hacer una inyección de campo con @Inject, con esto el componente pide una instancia de Ordenador.

Nota: Las inyecciones de campos no pueden ser privadas.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var ordenador: Ordenador
override fun onCreate(....) {
println(ordenador.tengoOrdenador())
}

Y terminará imprimiendo Tengo una clase Ordenador

En el siguiente diagrama vamos a introducir 2 iconos:

  • Wifi como forma de pedir una instancia
  • Router como forma de proporcionar la clase.
Componente pide una instancia de Ordenador

Vale, ¿pero cómo proporciono las clases CableEnchufe y Luz?

Para ello vamos a crear Vinculaciones donde proporcionaremos instancias a través del constructor.

Vinculaciones

Vamos a crear la clase Luz, esta clase no dependerá de ninguna clase, al igual que antes, debemos anotarla con @Inject, ya que indicamos a Hilt como proporcionar la clase Luz. También vamos a crear un método.

class Luz @Inject constructor(){
fun tengoLuz(): String{
return "Tengo luz"
}
}

Ahora pasaremos a crear la clase CableEnchufe, se anotará también con @Inject y pediremos una instancia de Luz en el constructor. De paso crearemos otro método el cual accederá al método de la clase Luz.

class CableEnchufe @Inject constructor(private val luz: Luz){
fun tengoElectricidad(): String{
return "${luz.tengoLuz()}, electricidad"
}
}

Y por último modificamos la clase Ordenador pidiendo una instancia de CableEnchufe y finalizaremos la construcción del método.

class Ordenador @Inject constructor(private val cableEnchufe: CableEnchufe){
fun tengoOrdenador(): String{
return "${cableEnchufe.tengoElectricidad()} y mi ordenador funciona!"
}
}

El MainActivity se quedará igual y solo tendrá que volver a imprimir el método tengoOrdenador() de la clase Ordenador.
Resultado: Tengo luz, electricidad y mi ordenador funciona!

Componente pide una instancia de Ordenador junto con vinculaciones.

Entiendo, ¿y tengo alguna limitación con las vinculaciones por constructor?

Lamentablemente si, no siempre se puede inyectar mediante un constructor como hemos hecho hasta ahora, esto se puede dar se pueden dar en los siguientes casos, por ejemplo:

  • No puedes inyectar una interfaz en un constructor
  • No puedes inyectar una biblioteca externa (ejemplo: Retrofit)

¿Y de que manera puedo solucionar estas limitaciones?

Bienvenido a los módulos de Hilt 🤗

Módulos de Hilt

Un módulo Hilt es una clase anotada con @Module y este informa a Hilt como proporcionar instancias de ciertos tipos. Además al módulo tendremos que indicarle en que clase de Android (Activity, Fragment, etc..) se usará o instalará dicho módulo con la anotación @InstallIn

Hay 2 maneras de proporcionar instacias con los módulos Hilt, una de ellas es con @Binds y la otra es @Provides. Cada una resuelve un problema específico.

Inyectar instancias de interfaces con Binds

Para poder entender a la perfección como funciona binds vamos crear una idea sencilla.
Vamos a imaginarnos que tenemos una interfaz llamada UserService, después tendremos una clase UserServiceImpl que implementará la interfaz y después haremos una inyección de campo pidiendo una instancia de UserService (También podríamos hacerlo mediante Vinculación)

Creamos la interfaz UserService.

interface UserService {
fun helloWorld(): String
}

Creamos la clase UserServiceImpl implementando la interfaz recientemente creada UserService. Tendremos que anotarle@Inject, ya que el Módulo más tarde nos pedirá una instancia de la clase.

class UserServiceImpl @Inject constructor(): UserService{
override fun helloWorld(): String {
return "Hello World from UserServiceImpl"
}
}

Ahora vamos a crear el módulo que permitirá proporcionar una dependencia.

@InstallIn(ActivityComponent::class)
@Module
abstract class UserServiceModule {
@Binds
@ActivityScoped
abstract fun bindUserService(
userServiceImpl: UserServiceImpl
):UserService
}

Vamos a repasar este código:

  • @InstallIn proporcionará esa dependencia en los componentes Activity o inferior.
  • @Binds indica a Hilt que implementación usar cuando necesite proporcionar una instancia de Tapon
  • @ActivityScoped define el alcance de vinculación a un componente. Se queda en memoria hasta que se destruye ese componente.
  • Y el parametro de la funcion abstract fun bindUserService le dice que implementación proporcionar cuando pida una instancia a la interfaz UserService, en nuestro caso UserServiceImpl

Y gracias a esto nos permitirá pasarle una interfaz mediante la Vinculacion o Inyección de Campo.

Creamos la actividad UserServiceActivity y le pediremos una instancia de UserService mediante Inyección de campo.

@AndroidEntryPoint
class UserServiceActivity : AppCompatActivity() {
@Inject lateinit var userService: UserService override fun onCreate(...) {
...............
println(userService.helloWorld())
}
}

El diagrama final quedaría tal que así.

Container usando Módulo con Binds

¿Y que ocurrirá si tenemos diferentes implementaciones de la misma interfaz?

Nos lanzará un error diciendonos que tenemos “bindings duplicados”:

error: [Dagger/DuplicateBindings] com.rober.daggerhilttutorial.binds.EjemploUserService.UserService is bound multiple times

¿Y cómo podemos hacer uso de diferentes implementaciones?

Fácil, Hilt nos proporciona qualifiers”, es una anotación que usas para identificar un binding específico. Vamos a resumirlo muy brevemente siguiendo la idea de UserService.

Primero vamos a tener la idea de que tendremos 2 clases AuthServiceUno y AuthServiceDos. Estos implementarán la misma interfaz, para ello crearemos annotation class” para cada uno

@Qualifier
@Retention
(AnnotationRetention.BINARY)
annotation class UserServiceUno
@Qualifier
@Retention
(AnnotationRetention.BINARY)
annotation class UserServiceDos

Y en el módulo colocaremos la anotación correspondiente a cada uno

@InstallIn(ActivityComponent::class)
@Module
abstract class UserServiceModule {
@UserServiceUno
@Binds
@ActivityScoped
abstract fun bindUserServiceUno(
userServiceUnoImpl: UserServiceUnoImpl
):UserService
@UserServiceDos
@Binds
@ActivityScoped
abstract fun bindUserServiceDos(
userServiceDosImpl: UserServiceDosImpl
):UserService
}

Nota: En este caso también podríamos usar @Provides(veremos enseguida como funciona provides), ambos devuelven el mismo tipo pero los etiqueta como diferentes bindings

Y por último cuando queramos pedir una instancia de la interfaz tendremos que anotarlo en la vinculación o inyección de campo

@AndroidEntryPoint
class UserServiceActivity : AppCompatActivity() {

@UserServiceUno
@Inject lateinit var userService: UserService

override fun onCreate(savedInstanceState: Bundle?) {
...............
user_service_bind_text.text = userService.helloWorld()
println(userService.helloWorld())
}
}

El diagrama final sería el siguiente:

Container con differentes bindings que tienen la misma interfaz

Y para inyectar librerías externas, ¿funcionaría de la misma manera?

No, pero es casi igual! Vamos a entrar a ver la anotación @Provides

Inyectar librerías externas con Provides

Para inyectar librerías externas como por ejemplo Retrofit o Room tendremos que anotarlo con Provides.

Esta vez realizaremos un ejemplo más funcional con Retrofit, haciendo un request a una API y que este nos devuelva una lista de Usuarios.
La URL que haremos request será la siguiente: https://jsonplaceholder.typicode.com/todos

La entidad User con sus propiedades

data class User
(val userId: Long, val id: Long, val title: String, val completed:Boolean){}

Después crearemos una interfaz UserService que contendrá un método para realizar una petición a la URL anterior. Daros cuenta que colocamos “todos” ya que es el endpoint que solicitaremos los datos.

interface UserService{
@GET("todos")
fun listUsers(): Call<List<User>>
}

Y con todo esto ya estamos preparados para crear un Módulo, pero esta vez tendrá una pequeña diferencia

@InstallIn(ActivityComponent::class)
@Module
object RetrofitModule{
@Provides
fun provideRetrofit(): Retrofit =
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()

@Provides
fun provideUserService(retrofit: Retrofit): UserService {
return retrofit.create(UserService::class.java)
}
}

Vamos a repasar brevemento 2 puntos:

  • La primera función permitirá crear una instancia de Retrofit cuando la necesite
  • La segunda función pedirá una instancia de Retrofit que gracias a que hemos anotado @Provides en el anterior método se podrá obtener y crearemos una instancia de UserService

Y por último en la actividad pediremos una instancia de UserService que el módulo nos proporcionará.

@AndroidEntryPoint
class RetrofitActivity : AppCompatActivity() {

@Inject lateinit var userService: UserService

override fun onCreate(...) {
.........
........

val call = userService.listUsers()
call.enqueue(object : Callback<List<User>> {
override fun onFailure(call: Call<List<User>>, t: Throwable) {
TODO("Not yet implemented")
}

override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
Log.i("RetrofitActivity", "Users Response: ${response.body()}")
}
})
}
}

Las conclusiones que debemos sacar aquí es que gracias a @Provides podemos inyectar librerías externas y hacer uso de ellos cuando alguien la necesite. El diagrama final de esta parte quedaría exactamente así:

Container usando Provides con Retrofit

Y hasta aquí la guía de introducción donde cubro casi todo lo que nos ofrece Dagger Hilt.
Quedan unas cosas muy interesantes por cubrir con Dagger Hilt las cuales te recomiendo e invito a leerlo😊:

Código fuente

https://github.com/robercoding/DaggerHilt-Sample-Guide

Muchísimas gracias por leer el artículo, espero que realmente os haya servido de ayuda y cualquier crítica o duda que tengáis, podéis comentarlo a través de Twitter, Instagram o via email: robercoding@gmail.com

--

--