Conceptos básicos de Dagger

La inserción manual de dependencias o localizadores de servicios en una app para Android puede ser problemática según el tamaño del proyecto. Puedes limitar la complejidad de tu proyecto a medida que escala verticalmente mediante el uso de Dagger para administrar dependencias.

Dagger genera automáticamente un código que imita el código que habrías escrito a mano. Debido a que el código se genera en tiempo de compilación, se puede rastrear y tiene más rendimiento que otras soluciones basadas en reflejos, como Guice.

Beneficios de usar Dagger

Dagger te libera de escribir código estándar, lo que es tedioso y propenso a errores, porque te permite hacer lo siguiente:

  • Generar código AppContainer (grafo de aplicación) que implementaste manualmente en la sección DI manual.

  • Crear fábricas para las clases disponibles en el grafo de la aplicación. Así se cumplen internamente las dependencias.

  • Decidir si debes volver a usar una dependencia o crear una instancia nueva mediante el uso de alcances

  • Crear contenedores para flujos específicos, como lo hiciste con el flujo de acceso en la sección anterior, usando los subcomponentes de Dagger, lo que mejora el rendimiento de tu app y libera objetos en la memoria cuando ya no son necesarios.

Dagger hace automáticamente todo esto en el tiempo de compilación, siempre que declares las dependencias de una clase y especifiques cómo satisfacerlas mediante anotaciones. Dagger genera un código similar al que habrías escrito manualmente. A nivel interno, Dagger crea un grafo de objetos al que puede hacer referencia para encontrar la forma de proporcionar una instancia de una clase. Para cada clase del grafo, Dagger genera una clase factory-type que usa internamente a fin de obtener instancias de ese tipo.

Durante la compilación, Dagger revisa tu código y procede de la siguiente manera:

  • Compila y valida los grafos de dependencias, lo que garantiza lo siguiente:

    • Las dependencias de cada objeto se pueden satisfacer, por lo que no hay excepciones de tiempo de ejecución.
    • No existen ciclos de dependencia, por lo que no hay bucles infinitos.
  • Genera las clases que se usan en el tiempo de ejecución para crear los objetos reales y sus dependencias.

Un caso práctico simple en Dagger: Cómo generar una fábrica

A fin de demostrar cómo puedes trabajar con Dagger, crearemos una fábrica simple para la clase UserRepository que se muestra en el siguiente diagrama:

Define UserRepository de la siguiente manera:

Kotlin

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }

    ...
}

Agrega una anotación @Inject al constructor UserRepository para que Dagger sepa cómo crear un UserRepository:

Kotlin

// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    // @Inject lets Dagger know how to create instances of this object
    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

En el fragmento de código anterior, le estás indicando a Dagger lo siguiente:

  1. Cómo crear una instancia de UserRepository con el constructor @Inject anotado.

  2. Cuáles son sus dependencias: UserLocalDataSource y UserRemoteDataSource.

Ahora, Dagger sabe cómo crear una instancia de UserRepository, pero no sabe cómo crear sus dependencias. Si también anotas las otras clases, Dagger sabrá cómo crearlas:

Kotlin

// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

Java

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

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

Componentes de Dagger

Dagger puede crear un grafo de las dependencias de tu proyecto que puede usar para averiguar dónde debería obtener esas dependencias cuando las necesite. Para que Dagger haga esto, debes crear una interfaz y anotarla con @Component. Dagger crea un contenedor como lo habrías hecho con la inserción manual de dependencias.

Dentro de la interfaz @Component, puedes definir funciones que muestren instancias de las clases que necesitas (p. ej., UserRepository). @Component le indica a Dagger que genere un contenedor con todas las dependencias necesarias para satisfacer los tipos que expone. Esto se denomina componente de Dagger y contiene un grafo que consta de los objetos que Dagger sabe cómo proporcionar y sus respectivas dependencias.

Kotlin

// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be provided from the container
    fun repository(): UserRepository
}

Java

// @Component makes Dagger create a graph of dependencies
@Component
public interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be consumed from the graph
    UserRepository userRepository();
}

Cuando creas el proyecto, Dagger genera una implementación de la interfaz ApplicationGraph para ti: DaggerApplicationGraph. Con su procesador de anotaciones, Dagger crea un grafo de dependencias conformado por las relaciones entre las tres clases (UserRepository, UserLocalDatasource y UserRemoteDataSource) con un solo punto de entrada: obtener una instancia de UserRepository. Puedes usarlo de la siguiente manera:

Kotlin

// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()

Java

// Create an instance of the application graph
ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

// Grab an instance of UserRepository from the application graph
UserRepository userRepository = applicationGraph.userRepository();

Dagger crea una instancia nueva de UserRepository cada vez que se solicita.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository != userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository != userRepository2)

A veces, necesitas tener una instancia única de una dependencia en un contenedor. Es posible que desees realizar esta acción por varios motivos:

  1. Deseas que otros tipos que tienen este tipo como dependencia compartan la misma instancia, como varios objetos ViewModel en el flujo de acceso con el mismo LoginUserData.

  2. Crear un objeto es costoso y no te conviene crear una instancia nueva cada vez que se declare como dependencia (por ejemplo, un analizador JSON).

En el ejemplo, es posible que desees tener una instancia única de UserRepository disponible en el grafo para que, cada vez que solicites un UserRepository, obtengas la misma instancia. Esto es útil en tu ejemplo porque, en una aplicación real con un grafo de aplicación más complejo, puedes tener varios objetos ViewModel que dependan de UserRepository y no te resultará conveniente crear instancias nuevas de UserLocalDataSource y UserRemoteDataSource cada vez que se deba proporcionar el UserRepository.

Para hacerlo, debes pasar la misma instancia de UserRepository a los constructores de las clases ViewModel. Sin embargo, a Dagger debes informarle que quieres usar la misma instancia, ya que no estás escribiendo el código de forma manual. Esto se puede hacer con anotaciones de alcance.

Alcance con Dagger

Puedes usar anotaciones de alcance para limitar la vida útil de un objeto a la vida útil de su componente. Esto significa que se usa la misma instancia de dependencia cada vez que se debe proporcionar ese tipo de dependencia.

Para tener una instancia única de un UserRepository cuando solicitas el repositorio en ApplicationGraph, usa la misma anotación de alcance para la interfaz @Component y el UserRepository. Puedes usar la anotación @Singleton, que ya incluye el paquete javax.inject que usa Dagger:

Kotlin

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@Singleton
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

Como alternativa, puedes crear y usar una anotación de alcance personalizado. Puedes crear una anotación de alcance de la siguiente manera:

Kotlin

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Java

// Creates MyCustomScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

Luego, puedes usarla como antes:

Kotlin

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val service: UserService
) { ... }

Java

@MyCustomScope
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

@MyCustomScope
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

En ambos casos, se proporciona el objeto con el mismo alcance que se usa para anotar la interfaz @Component. Por lo tanto, cada vez que llamas a applicationGraph.repository(), obtienes la misma instancia de UserRepository.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository == userRepository2)

Conclusión

Es importante que conozcas los beneficios y los conceptos básicos del funcionamiento de Dagger antes de usar esta herramienta en situaciones más complejas.

En la página siguiente, aprenderás a agregar Dagger a una aplicación para Android.