Gradle ofrece la posibilidad de crear proyectos de varios módulos.
En un proyecto de varios módulos que se envía como un solo APK sin módulos de funciones, es común tener un módulo app
que puede depender de la mayoría de los módulos del proyecto y un módulo base
o core
del que suelen depender los demás módulos. En general, el módulo app
contiene la clase Application
, mientras que el módulo base
contiene todas las clases comunes que se comparten entre los distintos módulos de tu proyecto.
El módulo app
es un buen lugar para declarar el componente de la aplicación (por ejemplo, ApplicationComponent
en la imagen a continuación) que puede proporcionar los objetos que otros componentes podrían necesitar, además de los singletons de tu app. Por ejemplo, el ApplicationComponent
definido en el módulo app
proporcionará clases como OkHttpClient
, analizadores de JSON, descriptores de acceso para la base de datos y objetos SharedPreferences
que se pueden definir en el módulo core
.
En el módulo app
, también puedes tener otros componentes con una vida útil más corta.
Un ejemplo podría ser un UserComponent
con una configuración específica de usuario (como una UserSession
) después de un acceso.
En los diferentes módulos del proyecto, puedes definir al menos un subcomponente que tenga una lógica específica para ese módulo, como se muestra en la figura 1.
Por ejemplo, en un módulo login
, podrías tener un LoginComponent
con alcance establecido mediante una anotación @ModuleScope
personalizada capaz de proporcionar objetos comunes a esa característica, como un LoginRepository
. Dentro de ese módulo, también puedes tener otros componentes que dependan de un LoginComponent
con un alcance personalizado diferente, por ejemplo, @FeatureScope
para un LoginActivityComponent
o un TermsAndConditionsComponent
donde puedes establecer el alcance de lógicas más específicas según las funciones, como los objetos ViewModel
.
Para otros módulos como Registration
, tendrías una configuración similar.
Como regla general, en un proyecto de varios módulos, los módulos del mismo nivel no deberían depender unos de otros. De ser así, considera si la lógica compartida (las dependencias mutuas) debería formar parte del módulo superior. En ese caso, refactoriza el proyecto para mover las clases al módulo superior; en caso contrario, crea un módulo nuevo que extienda el módulo superior y haz que ambos módulos originales extiendan el módulo nuevo.
Como práctica recomendada, generalmente deberías crear un componente en un módulo en los siguientes casos:
Cuando necesitas realizar una inserción de campo, como con
LoginActivityComponent
.Cuando necesitas establecer el alcance de objetos, como con
LoginComponent
.
Aparte de estos casos, si necesitas indicarle a Dagger cómo proporcionar objetos desde ese módulo, crea y expón un módulo de Dagger con los métodos @Provides
o @Binds
si la inserción de construcción no es posible para esas clases.
Implementación con subcomponentes de Dagger
La página de documentación Cómo usar Dagger en apps para Android incluye información sobre cómo crear y usar subcomponentes. Sin embargo, no puedes usar el mismo código porque los módulos de funciones no conocen el módulo app
. A modo de ejemplo, toma un flujo de Login típico y el código de la página anterior, y verás que la compilación ya no puede completarse:
Kotlin
class LoginActivity: Activity() { ... 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) ... } }
Java
public class LoginActivity extends Activity { ... @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); ... } }
Eso se debe a que el módulo login
no conoce MyApplication
ni appComponent
. Para que funcione, deberías definir una interfaz en el módulo de funciones que proporcione un FeatureComponent
que MyApplication
necesite implementar.
En el siguiente ejemplo, puedes definir una interfaz LoginComponentProvider
que proporcione un LoginComponent
en el módulo login
para el flujo de Login:
Kotlin
interface LoginComponentProvider { fun provideLoginComponent(): LoginComponent }
Java
public interface LoginComponentProvider { public LoginComponent provideLoginComponent(); }
Ahora, LoginActivity
usará esa interfaz en lugar del fragmento de código definido previamente:
Kotlin
class LoginActivity: Activity() { ... override fun onCreate(savedInstanceState: Bundle?) { loginComponent = (applicationContext as LoginComponentProvider) .provideLoginComponent() loginComponent.inject(this) ... } }
Java
public class LoginActivity extends Activity { ... @Override protected void onCreate(Bundle savedInstanceState) { loginComponent = ((LoginComponentProvider) getApplicationContext()) .provideLoginComponent(); loginComponent.inject(this); ... } }
Ahora, MyApplication
necesita implementar esa interfaz y los métodos requeridos:
Kotlin
class MyApplication: Application(), LoginComponentProvider { // Reference to the application graph that is used across the whole app val appComponent = DaggerApplicationComponent.create() override fun provideLoginComponent(): LoginComponent { return appComponent.loginComponent().create() } }
Java
public class MyApplication extends Application implements LoginComponentProvider { // Reference to the application graph that is used across the whole app ApplicationComponent appComponent = DaggerApplicationComponent.create(); @Override public LoginComponent provideLoginComponent() { return appComponent.loginComponent.create(); } }
Así es como puedes usar subcomponentes de Dagger en un proyecto de varios módulos. Con los módulos de funciones, la solución es diferente por la forma en que los módulos dependen unos de otros.
Dependencias de componentes con módulos de funciones
Con los módulos de funciones, se invierte la forma en que los módulos suelen depender entre sí. En lugar de que el módulo app
incluya módulos de funciones, los módulos de funciones dependen del módulo app
. Consulta una representación de cómo se estructuran los módulos en la figura 2.
En Dagger, los componentes deben conocer sus subcomponentes. Esta información se incluye en un módulo de Dagger que se agrega al componente superior (como el módulo SubcomponentsModule
en Cómo usar Dagger en apps para Android).
Lamentablemente, con la dependencia invertida entre la app y el módulo de funciones, el subcomponente no es visible desde el módulo app
porque no está en la ruta de compilación. Como ejemplo, un LoginComponent
definido en un módulo de funciones login
no puede ser un subcomponente del ApplicationComponent
definido en el módulo app
.
Dagger tiene un mecanismo llamado dependencias de componentes que puedes usar para resolver este problema. En lugar de ser un subcomponente del componente superior, el componente secundario depende de aquel. Así, no existe relación componente superior/componente secundario. Ahora, los componentes dependen de otros para obtener algunas dependencias. Los componentes deben exponer tipos del grafo para que los componentes dependientes los consuman.
Por ejemplo, un módulo de funciones llamado login
quiere crear un LoginComponent
que dependa del AppComponent
disponible en el módulo app
de Gradle.
A continuación se muestran las definiciones de las clases y el AppComponent
que forman parte del módulo app
de Gradle:
Kotlin
// UserRepository's dependencies class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor() { ... } // UserRepository is scoped to AppComponent @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } @Singleton @Component interface AppComponent { ... }
Java
// UserRepository's dependencies public class UserLocalDataSource { @Inject public UserLocalDataSource() {} } public class UserRemoteDataSource { @Inject public UserRemoteDataSource() { } } // UserRepository is scoped to AppComponent @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; } } @Singleton @Component public interface ApplicationComponent { ... }
En tu módulo login
de Gradle que incluye el módulo app
de Gradle, tienes una LoginActivity
que necesita la inserción de una instancia LoginViewModel
:
Kotlin
// LoginViewModel depends on UserRepository that is scoped to AppComponent class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
// LoginViewModel depends on UserRepository that is scoped to AppComponent public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
LoginViewModel
tiene una dependencia en UserRepository
que está disponible en AppComponent
y cuyo alcance se estableció con relación a ese componente. Creemos un LoginComponent
que dependa de AppComponent
para insertar una LoginActivity
:
Kotlin
// Use the dependencies attribute in the Component annotation to specify the // dependencies of this Component @Component(dependencies = [AppComponent::class]) interface LoginComponent { fun inject(activity: LoginActivity) }
Java
// Use the dependencies attribute in the Component annotation to specify the // dependencies of this Component @Component(dependencies = AppComponent.class) public interface LoginComponent { void inject(LoginActivity loginActivity); }
LoginComponent
especifica una dependencia en AppComponent
agregándola al parámetro de dependencias de la anotación del componente. Como Dagger insertará LoginActivity
, agrega el método inject()
a la interfaz.
Cuando se crea un LoginComponent
, se debe pasar una instancia de AppComponent
. Usa la fábrica de componentes para hacerlo:
Kotlin
@Component(dependencies = [AppComponent::class]) interface LoginComponent { @Component.Factory interface Factory { // Takes an instance of AppComponent when creating // an instance of LoginComponent fun create(appComponent: AppComponent): LoginComponent } fun inject(activity: LoginActivity) }
Java
@Component(dependencies = AppComponent.class) public interface LoginComponent { @Component.Factory interface Factory { // Takes an instance of AppComponent when creating // an instance of LoginComponent LoginComponent create(AppComponent appComponent); } void inject(LoginActivity loginActivity); }
Ahora, LoginActivity
puede crear una instancia de LoginComponent
y llamar al método inject()
.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the Login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Gets appComponent from MyApplication available in the base Gradle module val appComponent = (applicationContext as MyApplication).appComponent // Creates a new instance of LoginComponent // Injects the component to populate the @Inject fields DaggerLoginComponent.factory().create(appComponent).inject(this) super.onCreate(savedInstanceState) // Now you can access loginViewModel } }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the Login graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Gets appComponent from MyApplication available in the base Gradle module AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent; // Creates a new instance of LoginComponent // Injects the component to populate the @Inject fields DaggerLoginComponent.factory().create(appComponent).inject(this); // Now you can access loginViewModel } }
LoginViewModel
depende de UserRepository
; y para que LoginComponent
pueda acceder a él desde AppComponent
, AppComponent
debe exponerlo en su interfaz:
Kotlin
@Singleton @Component interface AppComponent { fun userRepository(): UserRepository }
Java
@Singleton @Component public interface AppComponent { UserRepository userRepository(); }
Las reglas de establecimiento de alcance con componentes dependientes funcionan igual que con los subcomponentes. Como LoginComponent
usa una instancia de AppComponent
, no puede usar la misma anotación de alcance.
Si deseas establecer el alcance de LoginViewModel
con relación a LoginComponent
, debes hacerlo igual que antes con la anotación @ActivityScope
personalizada.
Kotlin
@ActivityScope @Component(dependencies = [AppComponent::class]) interface LoginComponent { ... } @ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
@ActivityScope @Component(dependencies = AppComponent.class) public interface LoginComponent { ... } @ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Prácticas recomendadas
ApplicationComponent
siempre debe estar en el móduloapp
.Crea componentes de Dagger en módulos si necesitas realizar una inserción de campo en ese módulo o si necesitas establecer el alcance de objetos para un flujo específico de la aplicación.
En los módulos de Gradle creados como utilidades o asistentes, y que no necesiten compilar un grafo (el motivo por el que se requeriría un componente de Dagger), crea y expón módulos de Dagger públicos con los métodos @Provides y @Binds de las clases que no admiten la inserción de constructores.
Si quieres usar Dagger en una app para Android con módulos de funciones, usa las dependencias de componentes a fin de acceder a dependencias provistas por el
ApplicationComponent
definido en el móduloapp
.