Inyección de dependencias en Android con Dagger 2

8
11548

En este tutorial vamos a ver cómo empezar a usar Dagger2 en nuestras aplicaciones Android.

0. Índice de contenidos

Dagger2 es un contenedor de dependencias desarrollado con la idea de solucionar algunos de los problemas que tenían otros contenedores de dependencias usados en Java.

1. Introducción

El principal cambio de Dagger es que mueve una gran parte del proceso de creación del grafo de dependencias a tiempo de compilación, consiguiendo así descubrir muchos de los errores que suelen ocurrir en tiempo de ejecución(runtime) antes de arrancar la aplicación.

Además esto permite un arranque de la aplicación mucho más rápido, ya que no necesita generar el grafo cada vez que se arranca la aplicación, esto es especialmente útil en aplicaciones móviles y en servicios que son desplegados continuamente, ya que este tipo de aplicaciones es especialmente sensible al tiempo de arranque.

Dagger consigue esto usando apt, lo que permite en tiempo de compilación analizar las diferentes anotaciones y generar el grafo de objetos y las clases necesarías para hacer la inyección de dependencias, capturando una buena parte de los errores asociados a los contenedores de dependencias (dependencias no satisfechas, dependencias circulares..).

2. Módulos y Componentes

Dagger se compone principalmente de módulos y componentes:

  • Los módulos contienen las diferentes dependencias, mediante una serie de métodos proveedores, se encargan de crear aquellas dependencias que requieran algún tipo de proceso especial a la hora de ser creadas. Para algunas clases no es necesario hacer ningún tipo de procesamiento antes de ser creadas, para esas basta con usar la etiqueta @Inject en el constructor. Un módulo puede estar asociado a uno o más componentes.
  • Los componentes son los encargados de inyectar las dependencias y pueden asociarse a distintos ciclos de vida de la aplicación.

3. Inyección de dependencias en Android

Android tiene varias particularidades que hacen más dificil usar contenedores de dependencias en nuestras aplicaciones, cómo por ejemplo:

  • No tenemos control sobre la creación de nuestras clases de entrada (Actividades), ni podemos sobreescribir los constructores, ya que Android las crea a partir de reflexión y no servirá de nada. El punto de entrada suele ser el método onCreate y onResume, por lo que es necesario adaptarse a esta circunstancia.
  • Aunque hoy en día la memoria ya no sea un problema en los dispositivos móviles, conviene no tener en memoria demasiados objetos en desuso, por lo que tener un solo grafo de objetos en aplicaciones muy grandes es perjudicial.
  • El sistema puede decidir matar la aplicación en cualquier momento para recuperar memoria, pero cuando el usuario pulse en la aplicación espera que esta se abra rápidamente, por lo que el arranque de esta ha de ser lo más rápido posible.

A continuación vamos a crear un proyecto de prueba en el que vamos a ver los primeros pasos para incluir Dagger en un proyecto Android. Tódo el código lo puedes encontrar en GitHub.

4. Dependencias

El primer paso es añadir las dependencias necesarias en Gradle, por un lado tenemos que añadir el plugin de apt en nuestro fichero build.gradle exterior.

dependencies {
    classpath 'com.android.tools.build:gradle:2.0.0-rc1'
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

Este nos permitirá usar el procesador de anotaciones de Java en la build de Gradle. El siguiente paso será aplicar este plugin en el fichero gradle del módulo donde vayamos a usar Dagger (en este caso el módulo app).

apply plugin: 'com.neenbedankt.android-apt'
....
....

dependencies {
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.google.dagger:dagger:2.0.2'
    apt 'com.google.dagger:dagger-compiler:2.0.2'
    provided 'javax.annotation:jsr250-api:1.0'
}

Esto incluirá Dagger cómo una dependencia normal y el compilador de Dagger cómo una dependencia apt, que será la encargada de hacer todo el pre-procesado de las anotaciones.

Con esto ya tendremos todas las dependencias necesarias para empezar a usar Dagger en nuestra aplicación.

5. Creando nuestro primer módulo de Dagger

Nuestro primer paso con Dagger será crear un módulo que encapsule algunas de las dependencias que nos proporciona el SDK de Android.

Un módulo se define cómo una clase Java anotada con @Module :

@Module
public class SystemModule {

    private final Application application;

    public SystemModule(Application application) {
        this.application = application;
    }

    @Provides
    Context provideContext(){
        return application;
    }

}

En este caso el módulo tiene la dependencia directa con la clase Application, la cual nos permitirá acceder al contexto de Android para sacar las dependencias necesarias del SDK.

Además tenemos el método anotado provideContext el cual provee de un contexto a todas aquellas clases que requieran el contexto de Android cómo dependencia.

6. Creando un componente de aplicación.

El siguiente paso será el de crear un componente que incluya el módulo de Sistema que acabamos de crear. Un componente se define cómo una interfaz anotada con @Component.

@Singleton
@Component(modules = {SystemModule.class})
public interface SystemComponent {
    void inject(MainActivity activity);
}

La anotación @Singleton define el ciclo de vida de este componente, en este caso el ciclo de vida es toda la aplicación, más adelante ahondaremos sobre el tema de los ciclos de vida de los componentes.

El compilador de Dagger será el encargado de implementar esta clase y generar el método inject el cual inyectará todas las dependencias en la clase deseada. Esto solo es necesario para clases que no cree Dagger, solucionando uno de los problemas de la inyección de dependencias en Android. Por ejemplo:

class ClassThatUsesContext {
    @Inject
    public ClassThatUsesContext(Context context) {
        // do something with the context
    }
}

Esta clase la creará Dagger en el momento en el que la marquemos cómo una dependencia de otra clase, pasando al constructor todas las dependencias que este tenga(siempre que estas se encuentren en el contenedor, ya sea teniendo un constructor con @Inject o un método que las (@Provides) provea en algún módulo).

Cómo he dicho anteriormente, es Dagger el encargado de crear la implementación de la interfaz de nuestro componente, esto lo haremos al crear la aplicación

public class Dagger2Application extends Application {

    private SystemComponent systemComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        this.systemComponent = DaggerSystemComponent.builder()
                .systemModule(new SystemModule(this))
                .build();
    }

    public SystemComponent getSystemComponent() {
        return this.systemComponent;
    }
}

Dagger nos habrá generado la clase DaggerSystemComponent, la cual es un Builder que nos permitirá proveer los módulos que tiene cómo dependencias nuestro Component. Por último proveemos de un método para obtener el componente, el cual nos permitirá tener acceso al método inject en las clases deseadas.

7. Inyectando dependencias en una Activity con Dagger

Una vez completados los pasos anteriores ya estamos listos para inyectar nuestras dependencias en el Activity, para ello tenemos que hacernos con una referencia a nuestro SystemComponent.

public class BaseActivity extends AppCompatActivity {
    public SystemComponent getSystemComponent() {
        return ((Dagger2Application) getApplication()).getSystemComponent();
    }
}

Con este sencillo método en nuestra BaseActivity podemos disponer del SystemComponent en todas las activities que dependen de ella.

public class MainActivity extends BaseActivity {

    @Inject
    ConnectivityManager connectivityManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getSystemComponent().inject(this);

        boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered();
        logger.log("Network is metered? " + activeNetworkMetered);
    }

}

Y tan solo nos quedaría llamar al método inject en el método onCreate, a partir de este método ya podríamos usar cualquiera de las dependencias inyectadas por Dagger, cómo puede verse en el ejemplo.

*El método inject en Dagger es síncrono, por lo que no hay problema en usarlo justo después

8. Manejo de Scopes y ciclos de vida de los componentes en Dagger2

Los Scopes permiten indicar el ciclo de vida de una dependencia, por ejemplo:

"Queremos que la dependencia UserPreferencesManager se inicie en el momento en el que el usuario hace login(o al inicio de la app si este está logado) y se destruya en el momento en el que el usuario hace logout."

Para conseguir esto con Dagger2 debemos crear un Scope personalizado, el Scope es una anotación con la que anotaremos las diferentes dependencias indicando el Scope que tiene esa dependencia. Un ejemplo de Scope sería @Singleton.

A continuación vamos a ver cómo crear un Scope personalizado en Dagger.

El primer paso es crear una anotación UserScope

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface UserScope {
}

En este caso queremos que nuestro componente de Usuario extienda del de sistema, es decir, que todas las dependencias declaradas por los módulos del componente de sistema estén disponibles para este componente. Para ello, el componente de Usuario se declarará cómo @Subcomponent

@UserScope
@Subcomponent(
        modules = {
                UserModule.class
        }
)
public interface UserComponent {
    void inject(UserActivity userActivity);
}

Además lo anotaremos con la anotación UserScope y le indicaremos que depende del módulo UserModule. Esta clase también tiene que tener un método inject por cada clase en la que sea inyectado.

Nuestro UserModule se creará a partir de un objeto User y existirá mientras el usuario está logado.

@Module
public class UserModule {

    private final User user;

    public UserModule(User user) {
        this.user = user;
    }

    @Provides
    User provideUser() {
        return this.user;
    }
}

Cómo hemos dicho con antelación, queremos que nuestro UserModule tenga también acceso a todas las clases que están en los módulos del SystemComponent, cómo UserComponent es un subcomponente de este, el método de creación es un poco distinto.

El primer paso es añadir el siguiente método al interfaz de SystemComponent:

UserComponent plus(UserModule userModule);

Y añadir un método a nuestra clase Application que permitirá crear el UserComponent:

public UserComponent createUserComponent(User user) {
    userComponent = systemComponent.plus(new UserModule(user));
    return userComponent;
}

En nuestra UserActivity creamos el componente, que podrá ser reutilizado hasta que el usuario realice logout:

public class UserActivity extends BaseActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        User user = (User) getIntent().getSerializableExtra("User");
        UserComponent userComponent = getApp().createUserComponent(user);
        userComponent.inject(this);
    }

    @Override
    public void onClick(View v) {
        // onLogoutClick
        getApp().releaseUserComponent();
        finish();
    }
}

9. Sobre los Scopes

Cómo hemos podido comprobar las Scopes no hacen automáticamente el trabajo por nosotros, en última instancia somos nosotros quienes tenemos que indicar el momento en el que empieza un Scope y en el momento en el que acaba. Un lector podría pensar, entonces, de qué nos sirven los Scopes. La respuesta es sencilla, el compilador nos protegerá de meter la pata, cómo por ejemplo en el siguiente ejemplo:

@Singleton
@Provides
ConnectivityManager provideConnectivityManager(UserPreferencesManager userPreferencesManager) {
    return (ConnectivityManager) application.getSystemService(Context.CONNECTIVITY_SERVICE);
}

La dependencia ConnectivityManager está declarada con un Scope de Singleton, es decir, que está pensada para que dure el mismo tiempo que la aplicación, en cambio UserPreferencesManager está declarada con un Scope de User, el compilador nos avisará de que esto no se puede hacer, y devolverá un error al intentar compilar el proyecto:

Error de compilacion

Por un lado nos avisa que el Scope de UserPreferencesManager es distinto y además que no encuentra ningún objeto User que poder inyectar, ya que este objecto sólo existe en el UserComponent.

10. Conclusiones

Cómo hemos podido ver Dagger requiere un poco más de trabajo que otros contenedores de dependencias cómo pueden ser Spring y Guice, y más cuando solemos usarlos dentro de nuestro framework web, que hace que se integre sin casi notarlo en nuestras clases. Android en este sentido presenta más dificultades, ya que el SDK de Android no fue pensado de cara a facilitar la inyección de dependencias. Las ventajas de tener un contenedor de dependencias son claras, desde el punto de vista del testing y la mantenibilidad del código, y ahora gracias a Dagger2 no hay excusa de no usar uno en Android :).

8 COMENTARIOS

  1. Excelente tutorial y bien explicado pero el repositorio en GitHud esta vació, por favor pueden subir el ejemplo para poder complementar con lo explicado en el post. Gracias.

  2. Muchas gracias por el tutorial. Me ha sido de mucha ayuda. Solo tengo una consulta, espero me puedas orientar un poco. Al seguir los pasos todo ha funcionado correctamente, pero se me ocurrio intentar inyectar otra clase en el MainActivity, expecificamente la clase WifiManager, y no me ha dejado compilar el proyecto, me lanza un error. Mi pregunta es. Por que si funciona con la clase ConnectivityManager y no con WifiManager? Gracias y saludos.

    • Hola,

      quizás el problema es que te falta añadir el @Provides a un método que devuelva el WifiManager.

      Al ser una dependencia que no es tuya y por lo tanto no poder anotar el constructor con la anotación @Inject, es necesario añadir el método @Provides que ofrezca una instancia de la clase seleccionada

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad