Inyección de dependencias con Dagger 2

Cuando hace dos años revisaba el código que escribí hace cinco, me avergonzaba. Hoy me averguenzo del que escribí hace dos. Los programadores buscamos continuamente mejorar nuestro código. Más calidad de código significa menos errores, mantenimiento y evolución más sencillos y  el orgullo de un trabajo bien hecho.

Para conseguir un código de calidad debemos seguir los principios SOLID. Entre ellos el primero, “Single responsability”, consiste en que cada elemento de nuestro código, sea una clase o un método, se encargue de una sola cosa. Cumplir con este principio nos lleva a menudo a tener una gran cantidad de clases colaboradoras en las que delegamos responsabilidades concretas.

Manejar las instancias de estas clases no es fácil, y para ayudarnos está el quinto de los principios SOLID, “Dependency inversion”, que permite que nuestras clases no dependan de los colaboradores sino de abstracciones de estos. Hay diversas formas de conseguirlo, Factory classes, paso de dependencias por constructor, métodos inicializadores o inyección de dependencias (Dependency Injection o DI).

La inyección de dependencias es el más potente de estos métodos, pero tradicionalmente se ha visto como algo complicado. Gracias a Dagger 2 os enseñaré que nada más lejos. Programar es mucho más sencillo usando DI. En cuanto lo probéis no haréis jamás una aplicación, por simple que sea, sin DI.

Inyección básica

Java soporta la inyección de dependencias mediante anotaciones definidas en el JSR330. Se pueden inyectar dependencias (colaboradores) a una clase Java mediante la anotación @Inject por constructor, por propiedad o por parámetros de método, aunque este último caso se usa raramente.

public class MyClass {
   @Inject
   Presenter presenter;
  ….
}

public class Presenter {
   private final Navigator navigator
   @Inject
   public Presenter(Navigator navigator) {
       this.navigator = navigator;
   }
}

Siempre que se pueda, se recomienda la inyección por constructor antes que por propiedad ya que simplifica la sustitución de los colaboradores por mocks durante los tests unitarios. Sin embargo, no siempre tenemos acceso al constructor, por ejemplo cuando extendemos una Activity de Android.

Como veis no necesitamos preocuparnos en MyClass de cómo se instancia un Presenter ni qué de colaboradores necesita este a su vez. Simplemente indicamos que nos lo inyecte en nuestra propiedad presenter. En la clase Presenter indicamos que necesitaremos una instancia de Navigator, que nos llegará por constructor.

Fácil, ¿verdad? En realidad es un poco más complejo, pero no mucho. JSR330 define cómo funciona la inyección de dependencias, pero se necesita un framework de inyección que se ocupe de hacer el trabajo duro.

Hay varios disponibles:

  • Guice, que utiliza reflexión para realizar la inyección en tiempo de ejecución.
  • Dagger 2, que utiliza un procesador de anotaciones para generar clases factoría en tiempo de compilación. Esto hace que levantar nuestra aplicación sea mucho más rápido, especialmente cuando es grande.

¿Cómo configuramos Dagger 2 para nuestra aplicación?

Pondremos unos ejemplos para una aplicación Java genérica. Lo primero es añadir las dependencias y el Annotation processor.

En Gradle para Java:

plugins {
  id "net.ltgt.apt" version "0.10"
}
dependencies {
  compile 'com.google.dagger:dagger:2.11'
  apt 'com.google.dagger:dagger-compiler:2.11'
}

Y para Android:

dependencies {
  compile 'com.google.dagger:dagger:2.11'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.11'
}

Volviendo al ejemplo anterior, cuando el procesador de anotaciones de Dagger se encuentre @Inject Presenter presenter; creará una clase factoría capaz de instanciar ese Presenter tal como lo haríamos nosotros.

Aquí no hay magia. Para hacerlo, necesitará otra clase factoría para el Navigator que ha de pasarle al presenter, y así sucesivamente. Es decir, creará un Grafo de dependencias y las factorías que las satisfagan.

Es importante recordar que en Dagger toda clase que queramos que pueda ser inyectada deberá tener un constructor anotado con @Inject, aunque esté vacío:

public class Navigator {
   @Inject  public Navigator() { }
}

Ahora solo nos queda construir el grafo definido por los diversos @Inject. Para ello crearemos un interface anotado con @Component y un método inject para cada una de las raíces de nuestro grafo:

@Component
public interface DiComponent {
	void inject(MyClass myClass);
	...
}

La implementación de este interface tendrá el mismo nombre precedido por Dagger y método estático create() que nos proporciona el inyector:

public class MyClass {
   @Inject
   Presenter presenter;

   Public void initialize() {
       DaggerDiComponent.create().inject(this);
   }
                               }

En Android MyClass podría ser MainActivity y en el método onCreate() llamaríamos a:

DaggerDiComponent.create().inject(this);

¡Y ya está! Solo con las anotaciones y nuestro interface @Component tenemos Dagger 2 funcionando. El método inject(this) instanciará un Presenter y lo inyectará en MyClass. Para instanciar el Presenter primero instanciará un Navigator y se lo pasaré por constructor y así sucesivamente.

¿A que ya le habéis perdido el miedo a la inyección de dependencias? Ahora vamos a aprender más funcionalidades de Dagger, pero lo básico, aunque totalmente funcional, ya está listo para incorporarse a vuestros proyectos.

Inyección avanzada

Veamos algunas de las principales opciones avanzadas de inyección que nos proporciona Dagger:

@Provides

En algunas ocasiones no será posible la instanciación de una clase de esta manera:

  • Los interfaces no se pueden construir.
  • Las clases de terceros no se pueden anotar.
  • Distintas implementaciones de un interface.

En estos casos debemos decirle a Dagger cómo hacerlo. Para ello crearemos métodos anotados con @Provides dentro de una clase anotada con @Module:

@Module
public class MainModule {
@Provides
IAppCollaborator provideAppCollaborator() {
   return new AppCollaborator();
}
}

Podríamos devolver diferentes implementaciones de IAppCollaborator dependiendo de cualquier parámetro de nuestra aplicación simplificando la lógica de esta, como por ejemplo instancias diferentes para los distintos entornos.

Un método provides puede recibir a su vez dependencias como parámetros de método que Dagger se encargará de satisfacer y utilizarlas para instanciar el objeto en el constructor, decidir qué implementación crear, etc.

Podemos tener uno o varios Modules. Para incluirlos en el Grafo bastará con añadirlos a la anotación del Component:

@Component (modules = { ApplicationModule.class })
public interface DiComponent {
	void inject(MyClass myClass);
}

@Singleton

Si deseamos que las instancia que nos proporciona Dagger 2 sea Singleton bastará con anotar la clase o el método provides con @Singleton:

@Provides
@Singleton
IAppCollaborator provideAppCollaborator() {
   return new AppCollaborator();
}

O bien:

@Singleton
public class AppCollaborator implements IAppCollaborator {

@Named

En ocasiones necesitaremos inyectar distintas implementaciones de un interface por lo que usaremos varios métodos @Provides anotándolos con @Named:

@Provides
@Named("Production")
IApiService provideApiService() {
   return new ApiService(Environment.PRODUCTION);
}

@Provides
@Named("PreProduction")
IApiService provideApiService() {
   return new ApiService(Environment.PREPRODUCTION);
}

Al inyectarlo indicaremos que implementacion queremos:

@Inject
public AppCollaborator(@Named("Production") IApiService service) {

Lazy

Si el coste de instanciar un objeto es alto y no siempre se llega a utilizar, podemos indicar su instanciación como Lazy y no se creará hasta la primera vez que se utilice:

		@Inject Lazy<IApiService> service;

Provider

En ocasiones queremos una instancia nueva del objeto cada vez que la utilicemos. Para ello usamos un Provider:

@Inject Provider<IApiService> service;

public void call(String endpoint){
   service.get().call(endpoint);
}

Scoped bindings (Dependent components)

A veces deseamos que determinados objetos o colaboradores estén disponibles sólo dentro de un contexto. En Android podemos querer colaboradores que solo estén mientras exista la Activity actual o en un servidor web podemos necesitar que existan en el contexto del usuario logueado.

Para crear scoped bindings necesitaremos una anotación que nos defina el Scope. Podemos crear tantas como necesitemos, por usuario, por servicio, etc:

@Scope
@Retention(RUNTIME)
public @interface PerActivity {
}

Y crearemos un Component para cada Scope a través de esta anotación (debemos indicar la dependencia del Component padre):

@PerActivity
@Component(
       dependencies = DiComponent.class,
       modules = ActivityModule.class
)
public interface ActivityComponent {
   void inject(MainActivity mainActivity);
}

Como ahora el método inject para el root del Scope ya no está en el root del Grafo, lo quitaremos de allí. Cualquier dependencia proporcionada por el root tiene que ser indicada si la han de poder utilizar también los Scopes:

@Component
public interface DiComponent {
	//Exposed to sub-graphs
IAppCollaborator provideAppCollaborator();
}

El Component root lo instanciaremos en la clase principal de la app (Main en Java o Application en Android) y lo haremos accesible para que los Scopes se instancien desde él:

public DiComponent diComponent = DaggerDiComponent.create();

En la clase raíz del Scope podremos inyectar el Scoped Component, pero en vez del create() usaremos un builder con el fin de poder hacer un set al Scoped component del root component:

DaggerActivityComponent
       .builder()
       .diComponent(diComponent)
       .build()
       .inject(this);

Subcomponents

Los Subcomponents nos permiten organizar el Grafo en particiones, bien para organizarlo mejor o para poder usar un mismo Component en dos Scopes. Son Components que heredan de un Component padre:

@Subcomponent(modules = ActivityModule.class)
public interface ActivityComponent {
   void inject(MainActivity mainActivity);
}

En el componente raíz crearemos un método de nombre arbitrario para añadir el subcomponente:

@Component(modules = { DiModule.class })
public interface DiComponent {
   ActivityComponent plus(ActivityModule activityModule);
}

El component root lo instanciamos como lo hacíamos para los Scopes. Para inyectar en el subgrafo Subcomponent utilizaremos el método declarado en el Component padre:

diComponent
       .plus(new ActivityModule())
       .inject(this);

Builder vs Create

En ocasiones usamos un builder para instanciar el Component, mientras que otras veces usamos el método create(). Ya hemos usado el Builder para los Scoped components.
El motivo de usar un builder es que nos proporciona mayor flexibilidad. Por ejemplo, cuando los Modules tienen constructores, nos permite inicializarlos.

El siguiente ejemplo es habitual en Android. Inicializa un ApplicationComponent en una clase que hereda de Apllication y le pasa this. Así el component podrá hacer un Provides del ApllicationContext:

DaggerApplicationComponent.builder()
       .applicationModule(new ApplicationModule(this))
       .build();

Cómo seguir

Con lo comentado aquí se cubren la gran mayoría de los casos de uso de Dagger. Existen conceptos adicionales, como los builders de los subcomponentes o el nuevo Android Injector. Cada uno tiene sus ventajas e inconvenientes.

Dagger 2 es perfectamente utilizable también con Kotlin. Como ejemplo os dejo un enlace a Karchitec, una app Kotlin que lo usa. Y por supuesto está la documentación de Dagger 2.

He creado un proyecto “Playground” Java Android para que juguéis con todos ellos. Tiene cuatro ramas Git para los diversos modos de uso hemos visto o comentado y un quiz en el Readme de cada rama para revisar la comprensión de los conceptos.

¡Ya no hay excusa para que no lo probéis!

Miguel Sesma es principalmente desarrollador y formador Android desde 2010, apasionado de la calidad del software y comprometido con las arquitecturas clean y la introducción de buenas prácticas en el desarrollo de movilidad. Sus intereses van desde los lenguajes modernos como Kotlin y Swift aplicados al desarrollo móvil, programación funcional, reactiva, hasta investigación de nuevas tecnologías como los ordenadores cuánticos. Anteriormente ha trabajado Network and Database administrator, desarrollador C++ y desarrollo de IoT.

Ver toda la actividad de Miguel Sesma

Escribe un comentario