En la página Conceptos básicos de Dagger, se explica cómo puede ayudarte esta herramienta a automatizar la inserción de dependencias en tu app. Con Dagger, no es necesario escribir código estándar pesado y propenso a errores.
Resumen de prácticas recomendadas
- Usa la inyección de constructor con
@Inject
para agregar tipos al grafo de Dagger siempre que sea posible. Cuando no sea posible:- Usa
@Binds
para indicarle a Dagger qué implementación debería tener una interfaz. - Usa
@Provides
para indicarle a Dagger cómo proporcionar clases que no sean de propiedad de tu proyecto.
- Usa
- Solo debes declarar los módulos una vez en un componente.
- Nombra las anotaciones de alcance según el ciclo de vida en la ubicación donde se usa la anotación. Entre los ejemplos, se incluyen
@ApplicationScope
,@LoggedUserScope
y@ActivityScope
.
Cómo agregar dependencias
Para usar Dagger en tu proyecto, agrega estas dependencias a tu aplicación en el archivo build.gradle
. La versión más reciente de Dagger se encuentra en este proyecto de GitHub.
Kotlin
plugins { id 'kotlin-kapt' } dependencies { implementation 'com.google.dagger:dagger:2.x' kapt 'com.google.dagger:dagger-compiler:2.x' }
Java
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Dagger en Android
Considera una app para Android de ejemplo con el grafo de dependencia de la figura 1.
En Android, sueles crear un grafo de Dagger que reside en tu clase de aplicación porque deseas que una instancia del grafo permanezca en la memoria mientras se ejecuta la app. De esta manera, se adjunta el grafo al ciclo de vida de la app. En algunos casos, es posible que también desees que el contexto de la aplicación esté disponible en el grafo. Para eso también necesitarías que el grafo esté en la clase Application
. Una de las ventajas de este enfoque es que el grafo está disponible para otras clases del framework de Android.
Además simplifica las pruebas, ya que te permite usar una clase Application
personalizada en ellas.
Debido a que la interfaz que genera el grafo está anotada con @Component
, puedes llamarla ApplicationComponent
o ApplicationGraph
. Por lo general, mantendrás una instancia de ese componente en tu clase Application
personalizada y la llamarás cada vez que necesites el grafo de la aplicación, como se muestra en el siguiente fragmento de código:
Kotlin
// Definition of the Application graph @Component interface ApplicationComponent { ... } // appComponent lives in the Application class to share its lifecycle class MyApplication: Application() { // Reference to the application graph that is used across the whole app val appComponent = DaggerApplicationComponent.create() }
Java
// Definition of the Application graph @Component public interface ApplicationComponent { } // appComponent lives in the Application class to share its lifecycle public class MyApplication extends Application { // Reference to the application graph that is used across the whole app ApplicationComponent appComponent = DaggerApplicationComponent.create(); }
Debido a que el sistema crea instancias de ciertas clases de framework de Android, como actividades y fragmentos, Dagger no puede crearlas por ti. Para actividades específicas, todo el código de inicialización debe incluirse en el método onCreate()
.
Eso significa que no puedes usar la anotación @Inject
en el constructor de la clase (inyección de constructor) como hiciste en los ejemplos anteriores. En su lugar, debes usar la inserción de campo.
En lugar de crear las dependencias que requiere una actividad del método onCreate()
, te conviene que Dagger rellene esas dependencias por ti. Para la inyección de campo, en cambio, aplica la anotación @Inject
en los campos que deseas obtener del grafo de Dagger.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; }
Para simplificar, LoginViewModel
no es un ViewModel de componentes de la arquitectura de Android; es solo una clase normal que actúa como un ViewModel.
Para obtener más información sobre cómo inyectar estas clases, consulta el código de la implementación oficial de Android Blueprints Dagger, en la rama dev-dagger.
Con relación a Dagger, debes tener en cuenta que los campos inyectados no pueden ser privados. Como mínimo, deben tener visibilidad de paquete privado, como en el código anterior.
Actividades de inserción
Dagger necesita saber que LoginActivity
debe acceder al grafo para proporcionar el ViewModel
que requiere. En la página Conceptos básicos de Dagger, usaste la interfaz @Component
para obtener objetos del grafo exponiendo funciones con el tipo de datos que se muestra para lo que deseas obtener del grafo. En este caso, debes informar a Dagger sobre un objeto (en este caso, LoginActivity
) que requiere la inyección de una dependencia. Para eso, expones una función que tome como parámetro el objeto que solicita la inyección.
Kotlin
@Component interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is requesting. fun inject(activity: LoginActivity) }
Java
@Component public interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is injecting. void inject(LoginActivity loginActivity); }
Esta función le indica a Dagger que LoginActivity
quiere acceder al grafo y solicita la inyección. Dagger debe satisfacer todas las dependencias que requiere LoginActivity
(LoginViewModel
con sus propias dependencias).
Si tienes varias clases que solicitan inyección, debes declararlas todas específicamente en el componente con su tipo exacto. Por ejemplo, si LoginActivity
y RegistrationActivity
solicitan la inyección, habrá dos métodos inject()
en lugar de uno genérico que abarque ambos casos. El método inject()
genérico no le indica a Dagger qué se debe proporcionar. Las funciones que se incluyen en la interfaz pueden tener cualquier nombre, pero, por convención, en Dagger se suelen llamar inject()
cuando reciben el objeto para insertar como un parámetro.
Para inyectar un objeto en la actividad, deberías usar el appComponent
definido en tu clase Application
y llamar al método inject()
, pasando una instancia de la actividad que solicita la inserción.
A fin de evitar problemas relacionados con el restablecimiento de fragmentos, cuando uses actividades, inserta Dagger en el método onCreate()
de la actividad antes de llamar a super.onCreate()
. Durante la fase de restablecimiento en super.onCreate()
, una actividad agrega fragmentos que pueden requerir acceso a las vinculaciones de la actividad.
Cuando uses fragmentos, inyecta Dagger en el método onAttach()
del fragmento. En este caso, la inyección se puede hacer antes o después de llamar a super.onAttach()
.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Make Dagger instantiate @Inject fields in LoginActivity (applicationContext as MyApplication).appComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } } // @Inject tells Dagger how to create instances of LoginViewModel class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Make Dagger instantiate @Inject fields in LoginActivity ((MyApplication) getApplicationContext()).appComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } } public class LoginViewModel { private final UserRepository userRepository; // @Inject tells Dagger how to create instances of LoginViewModel @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Indiquémosle a Dagger cómo proporcionar el resto de las dependencias para compilar el grafo:
Kotlin
class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor( private val loginService: LoginRetrofitService ) { ... }
Java
public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } public class UserLocalDataSource { @Inject public UserLocalDataSource() {} } public class UserRemoteDataSource { private final LoginRetrofitService loginRetrofitService; @Inject public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) { this.loginRetrofitService = loginRetrofitService; } }
Módulos de Dagger
Para este ejemplo, usas la biblioteca de herramientas de redes de Retrofit.
UserRemoteDataSource
tiene una dependencia en LoginRetrofitService
. Sin embargo, la manera de crear una instancia de LoginRetrofitService
es diferente de la manera en que lo hiciste hasta ahora. No implica crear una instancia de clase; es el resultado de llamar a Retrofit.Builder()
y pasar parámetros diferentes para configurar el servicio de acceso.
Además de la anotación @Inject
, hay otra manera de indicarle a Dagger cómo proporcionar una instancia de una clase: la información incluida en los módulos de Dagger. Un módulo de Dagger es una clase con anotaciones @Module
. En él, puedes definir dependencias con la anotación @Provides
.
Kotlin
// @Module informs Dagger that this class is a Dagger Module @Module class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides fun provideLoginRetrofitService(): LoginRetrofitService { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) } }
Java
// @Module informs Dagger that this class is a Dagger Module @Module public class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides public LoginRetrofitService provideLoginRetrofitService() { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); } }
Los módulos son una forma de encapsular semánticamente información sobre cómo proporcionar objetos. Como puedes ver, se llamó a la clase NetworkModule
a fin de agrupar la lógica necesaria para proporcionar objetos relacionados con las herramientas de redes. Si se expande la aplicación, también puedes agregar la forma de proporcionar un OkHttpClient
aquí, o de configurar Gson o Moshi.
Las dependencias de un método @Provides
son los parámetros de ese método. Para el método anterior, se puede proporcionar LoginRetrofitService
sin dependencias porque el método no tiene parámetros. Si declaraste un OkHttpClient
como parámetro, Dagger deberá proporcionar una instancia de OkHttpClient
del grafo a fin de satisfacer las dependencias de LoginRetrofitService
. Por ejemplo:
Kotlin
@Module class NetworkModule { // Hypothetical dependency on LoginRetrofitService @Provides fun provideLoginRetrofitService( okHttpClient: OkHttpClient ): LoginRetrofitService { ... } }
Java
@Module public class NetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) { ... } }
Para que el grafo de Dagger sepa que existe este módulo, debes agregarlo a la interfaz @Component
de la siguiente manera:
Kotlin
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = [NetworkModule::class]) interface ApplicationComponent { ... }
Java
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = NetworkModule.class) public interface ApplicationComponent { ... }
Para agregar tipos al grafo de Dagger, se recomienda usar la inyección de constructor (es decir, con la anotación @Inject
en el constructor de la clase),
lo cual no siempre es posible, por lo que debes usar módulos de Dagger. Por ejemplo, esto sucede cuando quieres que Dagger use el resultado de un cálculo para determinar cómo crear la instancia de un objeto. Cada vez que Dagger tiene que proporcionar una instancia de ese tipo, ejecuta el código dentro del método @Provides
.
Así es como se ve el grafo de Dagger en el ejemplo en este momento:
El punto de entrada al grafo es LoginActivity
. Debido a que LoginActivity
inyecta LoginViewModel
, Dagger crea un grafo que sabe cómo proporcionar una instancia de LoginViewModel
y, de forma recurrente, de sus dependencias. Dagger sabe cómo hacerlo debido a la anotación @Inject
incluida en el constructor de las clases.
Dentro del ApplicationComponent
generado por Dagger, hay un método de tipo de fábrica para obtener instancias de todas las clases que sabe cómo proporcionar. En este ejemplo, Dagger delega al NetworkModule
incluido en ApplicationComponent
la obtención de una instancia de LoginRetrofitService
.
Alcances de Dagger
Los alcances se mencionaron en la página Conceptos básicos de Dagger; son una forma de contar con una instancia única de un tipo en un componente. Eso es lo que significa determinar el alcance de un tipo en relación con el ciclo de vida del componente.
Debido a que tal vez quieras usar UserRepository
en otras funciones de la app sin tener que crear un objeto nuevo cada vez, puedes designarlo como instancia única para toda la app. Lo mismo sucede con LoginRetrofitService
: además de que puede ser costoso crearlo, te conviene que se reutilice una instancia única de ese objeto. Crear una instancia de UserRemoteDataSource
no es tan costoso, de modo que no es necesario establecer su alcance en relación con el ciclo de vida del componente.
@Singleton
es la única anotación de alcance que se incluye con el paquete javax.inject
. Puedes usarla para anotar ApplicationComponent
y los objetos que deseas reutilizar en toda la aplicación.
Kotlin
@Singleton @Component(modules = [NetworkModule::class]) interface ApplicationComponent { fun inject(activity: LoginActivity) } @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } @Module class NetworkModule { // Way to scope types inside a Dagger Module @Singleton @Provides fun provideLoginRetrofitService(): LoginRetrofitService { ... } }
Java
@Singleton @Component(modules = NetworkModule.class) public interface ApplicationComponent { void inject(LoginActivity loginActivity); } @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; } } @Module public class NetworkModule { @Singleton @Provides public LoginRetrofitService provideLoginRetrofitService() { ... } }
Asegúrate de no generar fugas de memoria cuando apliques los alcances a los objetos. Siempre que el componente del alcance esté en la memoria, también se encontrará allí el objeto creado. Debido a que ApplicationComponent
se crea cuando se lanza la aplicación (en la clase Application
), se destruye cuando se destruye la app. Por lo tanto, la instancia única de UserRepository
siempre permanece en la memoria hasta que se destruye la aplicación.
Subcomponentes de Dagger
Si tu flujo de acceso (administrado por una sola LoginActivity
) consta de múltiples fragmentos, debes volver a usar la misma instancia de LoginViewModel
en todos los fragmentos. @Singleton
no puede anotar LoginViewModel
para reutilizar la instancia por las siguientes razones:
La instancia de
LoginViewModel
persistirá en la memoria después de que finalice el flujo.Necesitas una instancia diferente de
LoginViewModel
para cada flujo de acceso. Por ejemplo, si el usuario sale, lo mejor es utilizar una instancia diferente deLoginViewModel
en lugar de la misma instancia de cuando el usuario accedió por primera vez.
Para establecer el alcance de LoginViewModel
en relación con el ciclo de vida de LoginActivity
, debes crear un nuevo componente (un nuevo subgrafo) para el flujo de acceso y un nuevo alcance.
Creemos un grafo específico para el flujo de acceso.
Kotlin
@Component interface LoginComponent {}
Java
@Component public interface LoginComponent { }
Ahora, LoginActivity
debería recibir inyecciones de LoginComponent
porque tiene una configuración específica para el acceso. Esto quita la responsabilidad de inyectar LoginActivity
desde la clase ApplicationComponent
.
Kotlin
@Component interface LoginComponent { fun inject(activity: LoginActivity) }
Java
@Component public interface LoginComponent { void inject(LoginActivity loginActivity); }
LoginComponent
debe poder acceder a los objetos desde ApplicationComponent
porque LoginViewModel
depende de UserRepository
. Para indicarle a Dagger que quieres que un componente nuevo use parte de otro componente, debes emplear subcomponentes de Dagger. El nuevo componente debe ser un subcomponente del componente que contiene los recursos compartidos.
Los subcomponentes son componentes que heredan y extienden el grafo de objetos de un componente superior. Por lo tanto, todos los objetos proporcionados en el componente superior también se proporcionan en el subcomponente. De esta manera, un objeto de un subcomponente puede depender de un objeto proporcionado por el componente superior.
Para crear instancias de subcomponentes, necesitas una instancia del componente superior. Así, los objetos proporcionados por el componente superior al subcomponente conservan el alcance en relación con el componente superior.
En el ejemplo, debes definir LoginComponent
como subcomponente de ApplicationComponent
. Para hacerlo, anota LoginComponent
con @Subcomponent
:
Kotlin
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting fun inject(loginActivity: LoginActivity) }
Java
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent public interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting void inject(LoginActivity loginActivity); }
También debes definir una fábrica de subcomponentes dentro de LoginComponent
de modo que ApplicationComponent
sepa cómo crear instancias de LoginComponent
.
Kotlin
@Subcomponent interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { fun create(): LoginComponent } fun inject(loginActivity: LoginActivity) }
Java
@Subcomponent public interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { LoginComponent create(); } void inject(LoginActivity loginActivity); }
Para indicarle a Dagger que LoginComponent
es un subcomponente de ApplicationComponent
, debes hacer lo siguiente:
Crea un nuevo módulo de Dagger (por ejemplo,
SubcomponentsModule
) que pase la clase del subcomponente al atributosubcomponents
de la anotación.Kotlin
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent::class) class SubcomponentsModule {}
Java
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent.class) public class SubcomponentsModule { }
Agrega el nuevo módulo (es decir,
SubcomponentsModule
) aApplicationComponent
:Kotlin
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { }
Java
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = {NetworkModule.class, SubcomponentsModule.class}) public interface ApplicationComponent { }
Ten en cuenta que
ApplicationComponent
ya no necesita inyectarLoginActivity
, porque esa responsabilidad ahora le corresponde aLoginComponent
, de modo que puedes quitar el métodoinject()
deApplicationComponent
.Los usuarios de
ApplicationComponent
deben saber cómo crear instancias deLoginComponent
. El componente superior debe agregar un método a su interfaz para permitir que los usuarios creen instancias del subcomponente a partir de una instancia del componente superior:Expón la fábrica que crea instancias de
LoginComponent
en la interfaz:Kotlin
@Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent fun loginComponent(): LoginComponent.Factory }
Java
@Singleton @Component(modules = { NetworkModule.class, SubcomponentsModule.class} ) public interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent LoginComponent.Factory loginComponent(); }
Cómo asignar alcances a los subcomponentes
Si compilas el proyecto, puedes crear instancias de ApplicationComponent
y LoginComponent
. Se agrega ApplicationComponent
al ciclo de vida de la aplicación porque quieres usar la misma instancia del grafo mientras la aplicación está en la memoria.
¿Cuál es el ciclo de vida de LoginComponent
? Uno de los motivos por los que necesitabas LoginComponent
era que debías compartir la misma instancia de LoginViewModel
entre fragmentos relacionados con el acceso. Sin embargo, también te conviene usar instancias diferentes de LoginViewModel
cada vez que haya un nuevo flujo de acceso. LoginActivity
es el ciclo de vida adecuado para LoginComponent
: por cada actividad nueva, necesitas una instancia nueva de LoginComponent
y fragmentos que puedan usar esa instancia de LoginComponent
.
Debido a que LoginComponent
se adjuntó al ciclo de vida de LoginActivity
, debes mantener una referencia al componente en la actividad, de la misma manera que mantienes la referencia a applicationComponent
en la clase Application
. De esta manera, los fragmentos podrán acceder a él.
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent ... }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; ... }
Ten en cuenta que la variable loginComponent
no está anotada con @Inject
porque no esperas que Dagger proporcione esa variable.
Puedes usar el ApplicationComponent
para obtener una referencia a LoginComponent
y, luego, inyectar LoginActivity
de la siguiente manera:
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Creation of the login graph using the application graph loginComponent = (applicationContext as MyDaggerApplication) .appComponent.loginComponent().create() // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Creation of the login graph using the application graph loginComponent = ((MyApplication) getApplicationContext()) .appComponent.loginComponent().create(); // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } }
Se crea LoginComponent
en el método onCreate()
de la actividad y se destruye de forma implícita cuando finaliza la actividad.
El LoginComponent
debe proporcionar la misma instancia de LoginViewModel
cada vez que se le solicita. Para asegurarte de que lo haga, crea un alcance de anotación personalizado y anota tanto LoginComponent
como LoginViewModel
. Ten en cuenta que no puedes usar la anotación @Singleton
porque ya la utilizó el componente superior y eso convertiría al objeto en un singleton de aplicación (instancia única para toda la app). Debes crear un alcance de anotación diferente.
En este caso, el alcance se podría llamar @LoginScope
, pero no se considera una buena práctica. El nombre de la anotación del alcance no debe asociarse explícitamente con el propósito que cumple. En cambio, se le debe asignar un nombre según su ciclo de vida, ya que componentes secundarios como RegistrationComponent
y SettingsComponent
pueden reutilizar las anotaciones. Por este motivo, debes usar la denominación @ActivityScope
en lugar de @LoginScope
.
Kotlin
// Definition of a custom scope called ActivityScope @Scope @Retention(value = AnnotationRetention.RUNTIME) annotation class ActivityScope // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
// Definition of a custom scope called ActivityScope @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope {} // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent public interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Ahora, si tienes dos fragmentos que necesitan LoginViewModel
, ambos se proporcionan con la misma instancia. Por ejemplo, si tienes un LoginUsernameFragment
y un LoginPasswordFragment
, deben recibir una inyección de LoginComponent
:
Kotlin
@ActivityScope @Subcomponent interface LoginComponent { @Subcomponent.Factory interface Factory { fun create(): LoginComponent } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting fun inject(loginActivity: LoginActivity) fun inject(usernameFragment: LoginUsernameFragment) fun inject(passwordFragment: LoginPasswordFragment) }
Java
@ActivityScope @Subcomponent public interface LoginComponent { @Subcomponent.Factory interface Factory { LoginComponent create(); } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting void inject(LoginActivity loginActivity); void inject(LoginUsernameFragment loginUsernameFragment); void inject(LoginPasswordFragment loginPasswordFragment); }
Los componentes acceden a la instancia del componente que reside en el objeto LoginActivity
. El código de ejemplo de LoginUserNameFragment
aparece en el siguiente fragmento de código:
Kotlin
class LoginUsernameFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginUsernameFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Y lo mismo sucede con LoginPasswordFragment
:
Kotlin
class LoginPasswordFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginPasswordFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
En la figura 3, se muestra cómo se ve el grafo de Dagger con el nuevo subcomponente. Las clases con un punto blanco (UserRepository
, LoginRetrofitService
y LoginViewModel
) son las que tienen un alcance único en relación con sus componentes correspondientes.
Desglosemos las partes del grafo:
Se incluye el
NetworkModule
(y, por lo tanto,LoginRetrofitService
) enApplicationComponent
porque lo especificaste en el componente.UserRepository
permanece enApplicationComponent
porque el alcance esApplicationComponent
. Si se expande el proyecto, te conviene compartir la misma instancia en diferentes funciones (p. ej., Registro).Como
UserRepository
es parte deApplicationComponent
, sus dependencias (es decir,UserLocalDataSource
yUserRemoteDataSource
) también deben estar en este componente para que pueda proporcionar instancias deUserRepository
.Se incluye
LoginViewModel
enLoginComponent
porque solo es obligatorio para las clases inyectadas porLoginComponent
. No se incluyeLoginViewModel
enApplicationComponent
porque ninguna dependencia incluida enApplicationComponent
necesita unLoginViewModel
.Asimismo, si no hubieras definido el alcance de
UserRepository
paraApplicationComponent
, Dagger habría incluido automáticamenteUserRepository
y sus dependencias como parte deLoginComponent
, porque ese es el único lugar en el que se usaUserRepository
.
Además de definir el alcance de los objetos en relación con un ciclo de vida diferente, es recomendable crear subcomponentes para encapsular diferentes partes de la aplicación manteniendo la separación entre ellas.
Estructurar la app para crear diferentes subgrafos de Dagger según el flujo de la app ayuda a lograr una aplicación más escalable y de mejor rendimiento en cuanto a memoria y tiempo de inicio.
Prácticas recomendadas para compilar un grafo de Dagger
Cuando compiles el grafo de Dagger para tu aplicación, ten en cuenta lo siguiente:
Cuando creas un componente, debes considerar qué elemento es responsable de la vida útil de ese componente. En este caso, la clase
Application
está a cargo deApplicationComponent
, yLoginActivity
está a cargo deLoginComponent
.Emplea alcances solo cuando sea necesario. El uso excesivo de alcances puede tener un efecto negativo en el rendimiento del tiempo de ejecución de tu app: el objeto estará en la memoria mientras el componente esté en la memoria, y tener un objeto con alcance es más costoso. Cuando Dagger proporciona el objeto, usa el bloqueo de
DoubleCheck
, en lugar de un proveedor de tipo de fábrica.
Cómo probar un proyecto que usa Dagger
Uno de los beneficios de usar frameworks de inyección de dependencias, como Dagger, es que facilita la prueba del código.
Pruebas de unidades
No es necesario usar Dagger para las pruebas de unidades. Cuando se prueba una clase que usa la inyección de constructor, no necesitas usar Dagger para crear una instancia de esa clase. Puedes llamar a su constructor pasando dependencias falsas o simuladas directamente, tal como lo harías si no estuvieran anotadas.
Por ejemplo, cuando pruebes LoginViewModel
:
Kotlin
@ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... } class LoginViewModelTest { @Test fun `Happy path`() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository val viewModel = LoginViewModel(fakeUserRepository) assertEquals(...) } }
Java
@ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } } public class LoginViewModelTest { @Test public void happyPath() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository LoginViewModel viewModel = new LoginViewModel(fakeUserRepository); assertEquals(...); } }
Pruebas de extremo a extremo
Para las pruebas de integración, se recomienda crear un TestApplicationComponent
para pruebas.
La producción y las pruebas usan configuraciones de componentes distintas.
Así, se requiere un diseño de módulos más directo en la aplicación. El componente de prueba extiende el componente de producción e instala un conjunto diferente de módulos.
Kotlin
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class]) interface TestApplicationComponent : ApplicationComponent { }
Java
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // Component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class}) public interface TestApplicationComponent extends ApplicationComponent { }
FakeNetworkModule
tiene una implementación falsa del NetworkModule
original.
Allí puedes proporcionar instancias falsas o simulaciones de lo que quieras reemplazar.
Kotlin
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module class FakeNetworkModule { @Provides fun provideLoginRetrofitService(): LoginRetrofitService { return FakeLoginService() } }
Java
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module public class FakeNetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService() { return new FakeLoginService(); } }
En las pruebas de integración o de extremo a extremo, debes usar una TestApplication
que cree el TestApplicationComponent
, en lugar de un ApplicationComponent
.
Kotlin
// Your test application needs an instance of the test graph class MyTestApplication: MyApplication() { override val appComponent = DaggerTestApplicationComponent.create() }
Java
// Your test application needs an instance of the test graph public class MyTestApplication extends MyApplication { ApplicationComponent appComponent = DaggerTestApplicationComponent.create(); }
Luego, se usa la aplicación de prueba en un TestRunner
personalizado que emplearás para ejecutar pruebas de instrumentación. Si deseas obtener más información sobre este tema, consulta el codelab Cómo usar Dagger en tu app para Android.
Cómo trabajar con módulos de Dagger
Los módulos de Dagger son una forma de encapsular los procedimientos a fin de proporcionar objetos de manera semántica. Puedes agregar módulos en los componentes, pero también puedes incluirlos dentro de otros módulos. Si bien este es un procedimiento muy eficaz, es fácil caer en un uso inadecuado.
Una vez que se agrega un módulo a un componente o a otro módulo, se incorpora en el grafo de Dagger. Dagger puede proporcionar esos objetos al componente. Antes de agregar un módulo, comprueba si ya forma parte del grafo de Dagger. Para ello, verifica si ya se agregó al componente, o bien compila el proyecto y observa si Dagger puede encontrar las dependencias requeridas para ese módulo.
Se recomienda que se declaren los módulos solo una vez por un componente (fuera de los casos prácticos avanzados específicos de Dagger).
Supongamos que tienes tu grafo configurado de esta manera. ApplicationComponent
incluye Module1
y Module2
, mientras que Module1
incluye ModuleX
.
Kotlin
@Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = {ModuleX.class}) public class Module1 { ... } @Module public class Module2 { ... }
Ahora, Module2
depende de las clases proporcionadas por ModuleX
. No se recomienda incluir ModuleX
en Module2
, porque, de esta manera, ModuleX
aparece dos veces en el grafo, como se muestra en el siguiente fragmento de código:
Kotlin
// Bad practice: ModuleX is declared multiple times in this Dagger graph @Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module(includes = [ModuleX::class]) class Module2 { ... }
Java
// Bad practice: ModuleX is declared multiple times in this Dagger graph. @Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = ModuleX.class) public class Module1 { ... } @Module(includes = ModuleX.class) public class Module2 { ... }
En cambio, debes realizar una de las siguientes acciones:
- Refactoriza los módulos y extrae el módulo común del componente.
- Crea un módulo nuevo con los objetos que comparten ambos módulos y extráelo del componente.
Si no se refactoriza el código de esta manera, se genera una gran cantidad de módulos incluidos unos dentro de otros sin un sentido claro de organización y que hacen que sea más difícil ver de dónde proviene cada dependencia.
Recomendación (opción 1): Se declara ModuleX una vez en el grafo de Dagger.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleX::class]) interface ApplicationComponent { ... } @Module class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleX.class}) public interface ApplicationComponent { ... } @Module public class Module1 { ... } @Module public class Module2 { ... }
Recomendación (opción 2): Se extraen las dependencias comunes de Module1
y Module2
de ModuleX
y se incorporan a un nuevo módulo, llamado ModuleXCommon
, que se incluye en el componente. Luego, se crean otros dos módulos, llamados ModuleXWithModule1Dependencies
y ModuleXWithModule2Dependencies
, con las dependencias específicas de cada módulo. Se declaran todos los módulos una vez en el grafo de Dagger.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class]) interface ApplicationComponent { ... } @Module class ModuleXCommon { ... } @Module class ModuleXWithModule1SpecificDependencies { ... } @Module class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = [ModuleXWithModule1SpecificDependencies::class]) class Module1 { ... } @Module(includes = [ModuleXWithModule2SpecificDependencies::class]) class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class}) public interface ApplicationComponent { ... } @Module public class ModuleXCommon { ... } @Module public class ModuleXWithModule1SpecificDependencies { ... } @Module public class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = ModuleXWithModule1SpecificDependencies.class) public class Module1 { ... } @Module(includes = ModuleXWithModule2SpecificDependencies.class) public class Module2 { ... }
Inserción asistida
La inserción asistida es un patrón de DI que se usa para construir un objeto en el que el framework de DI puede proporcionar algunos parámetros y el usuario debe pasar otros en el momento de la creación.
En Android, este patrón es común en las pantallas de detalles, donde el ID del elemento que se muestra solo se conoce en el tiempo de ejecución, no en el tiempo de compilación, cuando Dagger genera el grafo de DI. Para obtener más información sobre la inserción asistida con Dagger, consulta la documentación de Dagger.
Conclusión
Si aún no lo hiciste, consulta la sección de prácticas recomendadas. Para ver cómo usar Dagger en una app para Android, consulta el codelab Cómo usar Dagger en tu app para Android.