Inyección de dependencias en Android

La inyección de dependencias (DI) es una técnica muy utilizada en programación y adecuada para el desarrollo de Android. Si sigues los principios de la DI, sentarás las bases para una buena arquitectura de apps.

Implementar la inyección de dependencias te proporciona las siguientes ventajas:

  • Reutilización de código
  • Facilidad de refactorización
  • Facilidad de prueba

Aspectos básicos de la inyección de dependencias

Antes de analizar específicamente la inyección de dependencias en Android, en esta página se proporciona una descripción más general de cómo funciona la inyección de dependencias.

¿Qué es la inyección de dependencias?

Las clases suelen requerir referencias a otras clases. Por ejemplo, una clase Car podría necesitar una referencia a una clase Engine. Estas clases se llaman dependencias y, en el ejemplo, la clase Car necesita una instancia de la clase Engine, de la que depende para ejecutarse.

Una clase puede obtener un objeto que necesita de tres maneras distintas:

  1. La clase construye la dependencia que necesita. En el ejemplo anterior, Car crea e inicializa su propia instancia de Engine.
  2. La toma de otro lugar. Algunas API de Android, como los métodos get de Context y getSystemService(), funcionan de esta manera.
  3. La recibe como parámetro. La app puede proporcionar estas dependencias cuando se construye la clase o pasarlas a las funciones que necesitan cada dependencia. En el ejemplo anterior, el constructor Car recibe Engine como parámetro.

La tercera opción es la inyección de dependencias. Con este enfoque, tomas las dependencias de una clase y las proporcionas en lugar de hacer que la instancia de la clase las obtenga por su cuenta.

Por ejemplo: Sin inyección de dependencias, la representación de un Car que crea su propia dependencia Engine se ve así en el código:

Kotlin

class Car {

    private val engine = Engine()

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

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
Clase Car sin inyección de dependencias

Este no es un ejemplo de inyección de dependencias, porque la clase Car está construyendo su propio Engine, lo que puede ser problemático debido a lo siguiente:

  • Car y Engine están estrechamente vinculados: una instancia de Car usa un tipo de Engine, y no se pueden utilizar subclases ni implementaciones alternativas con facilidad. Si el Car construyera su propio Engine, tendrías que crear dos tipos de Car en lugar de solo reutilizar el mismo Car para motores de tipo Gas y Electric.

  • La dependencia estricta de Engine hace que las pruebas sean más difíciles. Car usa una instancia real de Engine, lo que impide utilizar un doble de prueba y modificar Engine para diferentes casos de prueba.

¿Cómo se ve el código con la inyección de dependencias? En lugar de que las diferentes instancias de Car construyan su propio objeto Engine durante la inicialización, cada una recibe un objeto Engine como parámetro en su constructor:

Kotlin

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

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

Java

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}
Clase Car con inyección de dependencias

La función main usa Car. Debido a que Car depende de Engine, la app crea una instancia de Engine y, luego, la usa para construir una instancia de Car. Los beneficios de este enfoque basado en DI son los siguientes:

  • Reutilización de Car. Puedes pasar diferentes implementaciones de Engine a Car. Por ejemplo, puedes definir una nueva subclase de Engine, llamada ElectricEngine, para utilizar con Car. Si usas DI, solo debes pasar una instancia de la subclase actualizada de ElectricEngine y Car seguirá funcionando sin más cambios.

  • Prueba fácil de Car. Puedes pasar dobles de prueba para probar diferentes situaciones. Por ejemplo, puedes crear un doble de prueba de Engine, llamado FakeEngine, y configurarlo para diferentes pruebas.

Existen dos formas principales de realizar la inyección de dependencias en Android:

  • Inyección de constructor. Esta es la manera descrita anteriormente. Pasas las dependencias de una clase a su constructor.

  • Inyección de campo (o inyección de método set). El sistema crea instancias de ciertas clases de framework de Android, como actividades y fragmentos, por lo que no es posible implementar la inyección de constructor. Con la inyección de campo, se crean instancias de dependencias después de crear la clase. El código se vería así:

Kotlin

class Car {
    lateinit var engine: Engine

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

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

Java

class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

Inyección de dependencias automatizada

En el ejemplo anterior, creaste, proporcionaste y administraste por tu cuenta las dependencias de las diferentes clases, sin recurrir a una biblioteca. Esto se denomina inyección de dependencias a mano o inyección de dependencias manual. En el ejemplo de Car, solo había una dependencia, pero, si hay varias dependencias y clases, la inyección manual puede resultar más tediosa. Además, la inyección de dependencias manual presenta varios problemas:

  • En el caso de aplicaciones grandes, tomar todas las dependencias y conectarlas correctamente puede requerir una gran cantidad de código estándar. En una arquitectura de varias capas, para crear un objeto en una capa superior, debes proporcionar todas las dependencias de las capas que se encuentran debajo de ella. Por ejemplo, para construir un automóvil real, es posible que necesites un motor, una transmisión, un chasis y otras piezas; a su vez, el motor necesita cilindros y bujías.

  • Cuando no puedes construir dependencias antes de pasarlas (por ejemplo, si usas inicializaciones diferidas o solicitas permisos para objetos en los flujos de tu app), necesitas escribir y conservar un contenedor personalizado (o un grafo de dependencias) que administre las dependencias en la memoria desde el principio.

Hay bibliotecas que resuelven este problema automatizando el proceso de creación y provisión de dependencias. Se dividen en dos categorías:

  • Soluciones basadas en reflexiones que conectan las dependencias durante el tiempo de ejecución.

  • Soluciones estáticas que generan el código para conectar las dependencias durante el tiempo de compilación.

Dagger es una biblioteca de inserción de dependencias popular para Java, Kotlin y Android que mantiene Google. Dagger facilita el uso de la DI en tu app mediante la creación y administración del grafo de dependencias. Proporciona dependencias totalmente estáticas y en tiempo de compilación que abordan muchos de los problemas de desarrollo y rendimiento de las soluciones basadas en reflexiones, como Guice.

Alternativas a la inserción de dependencias

Una alternativa a la inserción de dependencias es usar un localizador de servicios. El patrón de diseño del localizador de servicios también mejora el desacoplamiento de clases de las dependencias concretas. En este procedimiento, creas una clase conocida como localizador de servicios que, a su vez, crea y almacena dependencias, y luego proporciona esas dependencias a pedido.

Kotlin

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

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

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

El patrón del localizador de servicios se diferencia de la inyección de dependencias en la forma en que se consumen los elementos. Con el patrón del localizador de servicios, las clases tienen el control y solicitan que se inyecten objetos. Con la inyección de dependencias, la app tiene el control e inyecta los objetos solicitados de manera proactiva.

En comparación con la inyección de dependencias:

  • La colección de dependencias que requiere un localizador de servicios hace que el código sea más difícil de probar, ya que todas las pruebas tienen que interactuar con el mismo localizador de servicios global.

  • Las dependencias se codifican en la implementación de la clase, no en la superficie de la API. De esta manera, es más difícil saber qué necesita una clase del exterior. Por ese motivo, los cambios en Car o las dependencias disponibles en el localizador de servicios pueden provocar que fallen las referencias, y así generar errores durante el tiempo de ejecución o en las pruebas.

  • Administrar los ciclos de vida de los objetos es más difícil si no quieres establecer alcances que abarquen el ciclo de vida completo de toda la app.

Cómo usar Hilt en tu app para Android

Hilt es la biblioteca de Jetpack recomendada para la inserción de dependencias en Android. Hilt establece una forma estándar de usar la inserción de dependencias en tu aplicación, ya que proporciona contenedores para cada clase de Android en tu proyecto y administra automáticamente sus ciclos de vida.

Hilt se basa en la popular biblioteca de inserción de dependencias Dagger y se beneficia de la corrección en tiempo de compilación, el rendimiento del entorno de ejecución, la escalabilidad y la compatibilidad con Android Studio que proporciona.

Para obtener más información sobre Hilt, consulta Inserción de dependencias con Hilt.

Conclusión

La inyección de dependencias le proporciona a tu app las siguientes ventajas:

  • Reutilización de clases y desacoplamiento de dependencias: Es más fácil cambiar las implementaciones de una dependencia. Se mejora la reutilización de código debido a la inversión del control, y las clases ya no controlan cómo se crean sus dependencias, sino que funcionan con cualquier configuración.

  • Facilidad de refactorización: Las dependencias se convierten en una parte verificable de la superficie de la API, por lo que pueden verificarse durante el tiempo de creación de objetos o el tiempo de compilación en lugar de ocultarse como detalles de implementación.

  • Facilidad de prueba: Una clase no administra sus dependencias, por lo que, cuando la pruebas, puedes pasar diferentes implementaciones para probar todos los casos diferentes.

Para comprender por completo los beneficios de la inserción de dependencias, debes probarla de forma manual en tu app, como se muestra en la sección Inserción de dependencias manual.

Recursos adicionales

Para obtener más información sobre la inyección de dependencias, consulta los siguientes recursos adicionales.

Ejemplos