Hoy entramos en un tema imprescindible en el desarrollo de apps pero que además tiene bastante complejidad y es muy posible que parezca «magia» al principio hasta que lo entendamos.

Qué es la inyección de dependencias

Se trata de un patrón de diseño que nos permite suministrar las instancias necesarias en cada una de las clases que lo requieran. No quiero ponerme muy técnico así que te daré un ejemplo muy sencillo.

Imagina la clase concierto. Dentro de esta tendrá la instancia de pianista, piano y público. La inyección de dependencias se encargaría de proveer al concierto todas esas instancias sin tener que dejarle esa responsabilidad a nuestra clase. Y no solo eso, si por ejemplo la clase pianista necesitara más clases para crearse, por ejemplo zapatos, pantalón y camisa, esta inyección se lo pasaría antes de crear la instancia necesaria.

Por decirlo de algún modo se trata de preparar por detrás la forma de devolver a cada clase todos los objetos que necesite sin tener que instanciarlo dentro de esta.

Y aunque no le veas mucha utilidad, es imprescindible para un desarrollo óptimo de proyectos grandes, para tests y mucho más.

Qué es Dagger Hilt

Hilt nace de Dagger como una librería que nos simplifica la inyección de dependencias en los proyectos Android y desde hace muy poco está producción, es decir, es seguro usarlo en nuestras aplicaciones.

Además Dagger Hilt forma parte de las integraciones de Android Jetpack, de decir, que es la propia recomendación de Google usar esta librería.

Tutorial Dagger Hilt en Kotlin con MVVP

Es el momento de empezar a programar y para ello usaré el proyecto con arquitectura MVVM que hemos creado en los capítulos anteriores.

Antes de continuar también quiero decirte que no hay una forma exacta de hacer inyección de dependencias así que yo te voy a explicar la que para mi gusta es la más sencilla de entender cuando se está empezando.

Así que con el proyecto abierto vamos a empezar añadiendo las dependencias necesarias. Vamos a ir a build.gradle general, y añadiremos el classpath de dagger hilt y su versión lo pondremos de variable para usarlo en todas las dependencias y evitar trabajar con versiones distintas.

buildscript {
    ext.kotlin_version = "1.3.72"
    ext.hilt_version = '2.35'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.3"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

Las líneas nuevas son las resaltadas.

Ahora si vamos al build.gradle del módulo app y lo primero que haremos será añadir las ids necesarias en la parte superior del fichero, dentro de plugins.

  id 'kotlin-kapt'
  id 'dagger.hilt.android.plugin'

Ahora si es el momento de añadir las dependencias necesarias. Vamos a la parte inferior y dentro de dependencies añadimos las dos librerías necesarias de Dagger Hilt.

    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"

Fíjate que como en el fichero anterior cree la variable hilt_version ya es capaz de encontrarla y así mantener la misma versión en todos.

También tienes que asegurarte de tener Java 8 en tu proyecto, o lo que es lo mismo, dentro de la etiqueta android{} de este mismo fichero tener lo siguiente.

compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

Ahora ya podemos sincronizar.

Configurar Dagger Hilt en Android

Inicializar Dagger Hilt es extremadamente sencillo. Lo primero que necesitaremos será tener una clase application, es decir, una clase que se ejecutará antes de empezar la app.

Crearla es similar a cualquier otra clase, vamos al directorio que queramos, en mi caso el principal. New>Kotlin Class/File y lo llamaremos MVVMExampleApp.

@HiltAndroidApp
class MvvmExampleApp:Application()

Para que sea una clase application solo tenemos que hacer que extienda de Application() como el ejemplo anterior. Y para que Dagger Hilt se configure, basta con añadir la anotación @HiltAndroidApp encima de la clase.

Solo nos quedaría ir al AndroidManifest para declarar que esa será la clase principal. Dentro de la llave <application añadiremos el atributo name junto a la ruta de la clase.

    <application
        android:allowBackup="true"
        android:name=".MvvmExampleApp"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MVVMExample">
        <activity android:name="com.cursokotlin.mvvmexample.ui.view.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

Con esto tendríamos el primer paso terminado.

Cómo funciona Dagger Hilt

Para poder inyectar clases se tienen que dar dos factores. Que la clase donde va a ser inyectada y la que va a ser inyectada estén preparadas con Dagger Hilt. Y esto puede cambiar dependiendo de los tipos. Pero vamos por pasos.

Inyectar activity con Dagger Hilt

El primer paso será preparar las activities y es tan sencillo como poner en la parte superior una etiqueta. Vamos a nuestra MainActivity y lo marcamos como entry point.

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

Con esto ya tendríamos preparada la activity y podríamos inyectarle dependencias (previamente preparadas) de la siguiente forma.

   @Inject
    lateinit var myClass:ExampleClass

Es tan sencillo como añadirle la etiqueta @Inject y definir una variable lateinit con la clase a inyectar. Eso sí, nunca pueden ser privadas.

Inyectar ViewModel con Dagger Hilt

Esa es la primera forma de inyectar clases, pero es solo para las activities y lo mas habitual será inyectar las clases el en ViewModel que será el que lleve la lógica de la activity.

Para preparar un ViewModel será casi tan sencillo. Fíjate nuestro ViewModel antes de tener inyección de dependencias.

class QuoteViewModel : ViewModel() {

    var getQuotesUseCase = GetQuotesUseCase()
    var getRandomQuoteUseCase = GetRandomQuoteUseCase()

    val quoteModel = MutableLiveData<QuoteModel>()
    val isLoading = MutableLiveData<Boolean>()

...

La parte que nos interesa es el principio, fíjate que contiene dos instancias de dos casos de usos. En este ejemplo es bastante sencillo ya que no requieren ningún valor extra pero aún así estamos instanciando los casos de uso dentro del ViewModel y de eso se encargará hilt.

@HiltViewModel
class QuoteViewModel @Inject constructor(
    private val getQuotesUseCase:GetQuotesUseCase,
    private val getRandomQuoteUseCase:GetRandomQuoteUseCase
) : ViewModel() {

    val quoteModel = MutableLiveData<QuoteModel>()
    val isLoading = MutableLiveData<Boolean>()

En este segundo ejemplo hemos preparado la inyección de dependencias añadiendo la etiqueta @HiltViewModel y poniendo @Inject contructor() después del nombre de la clase. A partir de aquí la mayoría de las clases se inyectarán sin etiqueta, solo añadiendo inject constructor() y dentro de estos paréntesis irán las clases inyectadas.

Inyectar clases normales

Si ejecutases ahora fallaría ya que comenté al principio la clase tiene que estar preparada para poder inyectar y las clases que se van a inyectar también tienen que estar preparadas y esos dos casos de uso no lo están.

Vamos a preparar los dos casos de uso y como comentaba solo debemos añadirle @Inject constructor. Vamos con GetQuotesUseCase.

class GetQuotesUseCase {
    private val repository = QuoteRepository()
    suspend operator fun invoke() = repository.getAllQuotes()
}

Así es como lo teníamos. Para que sea inyectable lo dejaríamos así.

class GetQuotesUseCase @Inject constructor(){
    private val repository = QuoteRepository()
    suspend operator fun invoke() = repository.getAllQuotes()
}

Esto ya estaría preparado para ser inyectado pero como ves, tiene una instancia dentro que se crea y no sería lo correcto, porque aunque funcione lo perfecto sería que el repositorio se inyectara a esta clase, así que sería hacer lo mismo, preparar QuoteRepository para que se pueda inyectar.

Entonces tendríamos no solo que preparar QuoteRepository, sino también añadir la inyección en el GetQuotesUseCase, así que lo terminaríamos de dejar así.

class GetQuotesUseCase @Inject constructor(private val repository: QuoteRepository) {
    suspend operator fun invoke() = repository.getAllQuotes()
}

El funcionamiento será igual a partir de aquí, podemos ir avanzando en el proyecto preparando todas las clases.

Inyectando con @Provide

Con lo aprendido anteriormente podemos inyectar cualquier clase que esté en nuestro proyecto o que no sea una interfaz. Pero ¿Que pasaría si queremos inyectar Retrofit? No podemos ir a la clase Retrofit y ponerle @Inject constructor() porque es una librería externa del proyecto y por ende no nos va a dejar escribir. Es por ello que podemos crear módulos que nos implementen dependencias más especiales.

Vamos al directorio principal, creamos un nuevo directorio llamado DI y dentro crearemos una clase llamada NetworkModule, que será la encargada de proveernos todo lo relacionado con la parte de las llamadas.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
}

Este sería nuestro primer módulo que actualmente no inyecta nada pero hay cosas que debemos ver. Lo primero es que es obligatorio añadirle la etiqueta @Module, ya que sino no proveerá nada.

Fíjate que contiene una segunda etiqueta llamada @IntallIn(), que será la encargada de definir el alcance de nuestras dependencias. ¿A qué me refiero? Cuando este módulo nos provea de alguna dependencia, lo que hará dagger será crear una instancia y esta no morirá hasta que se salga del alcance definido. Por ejemplo si necesitamos inyectar Retrofit en una activity (y ponemos alcance a nivel de activity), cada vez que esa clase pida Retrofit creará una instancia nueva, pero morirán cuando dicha activity muera.

alcance dagger hilt modulos tutorial inyeccion de dependencias kotlin mvvm
Diferentes alcances de los módulos.

Entonces si te fijas el alcance de nuestro módulo es @Singleton (No confundir con el patrón de diseño) por lo que las instancias que creemos no morirán hasta que muera la app.

Ahora dentro de este objeto añadiremos las dependencias que vamos a proveer a través de él, en este caso Retrofit.

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

    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://drawsomething-59328-default-rtdb.europe-west1.firebasedatabase.app/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

Aunque el nombre no sea importante te recomiendo que los nombres todos iguales provideNombreDeLoQueVasAProveer. También fíjate en dos cosas, para empezar tenemos que poner la etiqueta @Provide para que sea válido (ignoremos la etiqueta @Singleton por ahora) y luego tenemos que definir lo que vamos a proveer, en este caso Retrofit, así que lo creamos como siempre.

También podemos proveer la interfaz de Retrofit preparada, añadiendo dentro del mismo objeto otro provider.

  @Singleton
    @Provides
    fun provideQuoteApiClient(retrofit: Retrofit):QuoteApiClient{
        return retrofit.create(QuoteApiClient::class.java)
    }

Es muy similar al anterior, la única diferencia es que esta función recibe un objeto Retrofit y eso es debido a que como ya nos proveemos dicho objeto (con la función provideRetrofit), dagger sabe automáticamente de donde sacarlo.

La clase QuoteService contiene actualmente una variable que nos da la instancia de Retrofit y luego dentro de nuestra función le añadimos la interfaz para poder hacer una petición (si no sabes de lo que hablo te recomiendo que veas este capítulo).

Es por eso que ahora podemos volver a la clase QuoteService y dejarla inyectada, pasando de esto.

class QuoteService {

    private val retrofit = RetrofitHelper.getRetrofit()

    suspend fun getQuotes(): List<QuoteModel> {
        return withContext(Dispatchers.IO) {
            val response = retrofit.create(QuoteApiClient::class.java).getAllQuotes()
            response.body() ?: emptyList()
        }
    }

}

A esto.

class QuoteService @Inject constructor(private val api:QuoteApiClient) {
    suspend fun getQuotes(): List<QuoteModel> {
        return withContext(Dispatchers.IO) {
            val response = api.getAllQuotes()
            response.body() ?: emptyList()
        }
    }
}

Cuidado con las múltiples instancias

Por último quiero comentarte de los peligros de dagger y es que como ya he dicho más arriba cada vez que pedimos una inyección se creará un objeto nuevo por lo que su contenido puede variar. Por ejemplo con la clase QuoteProvider que se ha estado usando como pequeño sistema de almacenamiento.

Cuando hacemos la primera llamada al servidor, recogemos todas las citas y las almacenamos en QuoteProvider y luego en otro caso de uso vamos recuperando algunas de ellas. El problema es que ahora mismo no va a funcionar ya que son dos instancias distintas y una contiene las citas y otra no.

Para solucionar este tipo de problemas tan habituales es tan sencillo como poner la etiqueta @Singleton encima de las clases que queremos que mantengan una única instancia durante todo el proyecto.

@Singleton
class QuoteProvider @Inject constructor() {
    var quotes: List<QuoteModel> = emptyList()
}

Solo con esa etiqueta QuoteProvider mantendrá su única instancia durante todo el proyecto y con ello el contenido de sus variables. Fíjate que esto también lo podemos hacer en los módulos (vuelve al módulo de Retrofit y fíjate en ambas funciones).

Conclusión

Con lo aprendido anteriormente deberías ser capaz de poder inyectar cualquier proyecto por completo. Como en este capítulo no hemos ido clase por clase, puedes acceder a mi GitHub donde estará todo el proyecto organizado por ramas y además te recomiendo ver el capítulo en vídeo que es donde voy haciendo todo el proyecto clase por clase.

Continúa con el curso: Capítulo 40 – Testing en Android – Test unitarios


Te recuerdo que puedes seguirme en mis redes sociales en Aristi.Dev. Y si tienes dudas con este o cualquier otro artículo del blog únete al Discord de la comunidad y te ayudaremos.