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:
- La clase construye la dependencia que necesita. En el ejemplo anterior,
Car
crea e inicializa su propia instancia deEngine
. - La toma de otro lugar. Algunas API de Android, como los métodos get de
Context
ygetSystemService()
, funcionan de esta manera. - 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
recibeEngine
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(); } }
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
yEngine
están estrechamente vinculados: una instancia deCar
usa un tipo deEngine
, y no se pueden utilizar subclases ni implementaciones alternativas con facilidad. Si elCar
construyera su propioEngine
, tendrías que crear dos tipos deCar
en lugar de solo reutilizar el mismoCar
para motores de tipoGas
yElectric
.La dependencia estricta de
Engine
hace que las pruebas sean más difíciles.Car
usa una instancia real deEngine
, lo que impide utilizar un doble de prueba y modificarEngine
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(); } }
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 deEngine
aCar
. Por ejemplo, puedes definir una nueva subclase deEngine
, llamadaElectricEngine
, para utilizar conCar
. Si usas DI, solo debes pasar una instancia de la subclase actualizada deElectricEngine
yCar
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 deEngine
, llamadoFakeEngine
, 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.