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:
Cómo crear una instancia de
UserRepository
con el constructor@Inject
anotado.Cuáles son sus dependencias:
UserLocalDataSource
yUserRemoteDataSource
.
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:
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 mismoLoginUserData
.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.