Antes de empezar a ver cómo funciona la inyección de dependencias en Android ya sea con Dagger o con el nuevo Hilt, lo primero es recordar y poner un poco en actualidad que es esto de la inyección de dependencias.

La inyección de dependencias básicamente es un patrón de diseño de software que se utiliza, normalmente junto con la inversión de dependencias, para proporcionar instancias de objetos a otros objetos que las necesitan y dependen de estos otros para poder crearse. De ahí el termino dependencia.

De forma simple se podría decir que es un patrón que nos dice cómo tenemos que extraer la responsabilidad de creación de instancias de los objetos de los que depende otro para poder existir. Cuando entremos en los ejemplos esto se ve de forma mucho más clara.

Ejemplo de inyección de dependencias

Vamos a ver mediante un ejemplo muy sencillo que es la inyección de dependencias y que ventajas nos ofrece cuando programamos utilizándola frente a no utilizar este tipo de patrones. Imaginemos el siguiente código:

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

Aquí tenemos una clase Car que internamente crea una instancia de la clase Engine y la utiliza más adelante en su método start(). Esto es un claro ejemplo de cómo la clase Car depende de la clase Engine para poder existir. Es decir, si no tengo un motor, no puedo tener un coche. Car depende de Engine.

En este caso, esa dependencia se ha resuelto de forma interna instanciando la clase Engine directamente dentro de la clase Car. Pero esta forma de actuar implica una serie de inconvenientes que entre otros son:

  • Imposibilidad de mockear la clase Engine para poder realizar diferentes flujos de test
  • Acoplamiento total de la clase Car a la clase Engine

Estos inconvenientes los podríamos solucionar si en vez de instanciar la clase Engine directamente dentro de la clase Car, pasásemos una instancia ya creada utilizando el constructor de la clase Car.

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

En este segundo ejemplo la clase Car aunque sigue dependiendo de la clase Engine para existir y funcionar, su grado de acoplamiento es menor. Y esta dependencia sería mucho menor si Engine en vez de ser una clase fuese una interfaz. En este caso cualquier clase que implementase esa interfaz sería válida para ser utilizada por Car.

interface Engine{
    fun start()
}

class ElectricEngine: Engine{
    override fun start() {
        print("Electric Engine")
    }
}


class FossilEngine: Engine{
    override fun start() {
        print("Fossil Engine")
    }
}

class Car(private val engine: Engine){

    fun start(){
        engine.start()
    }
}

fun main() {
    
    val car = Car(engine = FossilEngine())
    car.start() //print Fosil Engine

    val car = Car(engine = ElectricEngine())
    car.start() //print Electric Engine
    
}

Como vemos en el código anterior, ahora Car puede utilizar cualquier clase que implemente la interfaz Engine. En este caso estamos utilizando tanto la inversión de dependencias, Car depende de una abstracción, Engine, y no de una concreción como sería EletricEngine o FossilEngine; junto con la inyección de dependencias. A la clase Car se le inyecta por constructor la instancia de la clase que queremos pasarle.

Ahora bien, esta inyección hasta aquí, la estamos haciendo de forma manual. Cogemos la clase ElectricEngine y la instanciamos en el constructor de Car de forma fácil, pero el problema está cuando una clase depende no de una, sino de muchas otras clases, y estas otras clases dependen a su vez de otras muchas clases. En estos casos la cosa se complica y es cuando vienen a ayudarnos los inyectores de dependencias.

Un inyector de dependencias no es más que una herramienta, librería, que nos ayuda en esta tarea de ir definiendo todas las dependencias de todas las clases de nuestro proyecto. En Android tenemos entre otros, Dagger y Hilt.

Y con ellos lo que hacemos es ir construyendo el grafo de jerarquía de las clases para que cuando sea necesario instanciar una de ellas y pasársela a la que lo necesite, sea el inyector el que haga el trabajo por nosotros.

Como hay que definir las dependencias de nuestras clases con uno y otro framework es lo que veremos en los próximos post.