Um projeto com vários módulos do Gradle é conhecido como um projeto multimódulo.
Em um projeto multimódulo enviado como um único APK sem módulos
de recursos, é comum ter um módulo app
que depende da maioria
dos módulos do projeto e base
ou core
que serve de base para os demais. O módulo app
normalmente contém sua
classe Application
, enquanto o base
contém todas as classes comuns compartilhadas em todos os módulos do projeto.
O módulo app
é um bom lugar para declarar o componente do aplicativo (por
exemplo, ApplicationComponent
na imagem abaixo) que pode fornecer objetos
de que outros componentes podem precisar, além dos singletons do app. Por exemplo,
classes como OkHttpClient
, analisadores JSON, acessadores para o banco de dados
ou objetos SharedPreferences
que podem ser definidos no core
são fornecidos pelo ApplicationComponent
definido no módulo app
.
No módulo app
, também é possível ter outros componentes com ciclos de vida mais curtos.
Um exemplo pode ser um UserComponent
com configuração específica do usuário
(como um UserSession
) após um login.
Nos diferentes módulos do projeto, você pode definir pelo menos um subcomponente com uma lógica específica para esse módulo, como na Figura 1.
Por exemplo, em um módulo login
, é possível ter um LoginComponent
com escopo de anotação @ModuleScope
personalizada que pode fornecer objetos comuns
a esse recurso, por exemplo, um LoginRepository
. Dentro desse módulo, você também pode
ter outros componentes que dependem de um LoginComponent
com um escopo
personalizado diferente, por exemplo, @FeatureScope
para um LoginActivityComponent
ou um
TermsAndConditionsComponent
em que é possível definir uma lógica de escopo mais específica ao recurso,
como objetos ViewModel
.
Para outros módulos, como Registration
, podemos observar uma configuração parecida.
Uma regra geral para um projeto multimódulo é que os módulos do mesmo nível não podem depender uns dos outros. Caso dependam, analise se essa lógica compartilhada (as dependências entre eles) precisa fazer parte do módulo pai. Se precisar, refatore para mover as classes para o módulo pai. Se não precisar, crie um novo módulo para estender o módulo pai e faça com que os dois módulos originais estendam o novo módulo.
Como prática recomendada, você geralmente criaria um componente em um módulo nos seguintes casos:
Você precisa realizar a injeção de campo, como em
LoginActivityComponent
.Você precisa fazer o escopo dos objetos, como em
LoginComponent
.
Se nenhuma dessas classes for aplicável e você precisar informar ao Dagger como fornecer
objetos desse módulo, crie e exponha um módulo do Dagger com métodos @Provides
ou
@Binds
caso a injeção de construção não possa ser feita.
Implementação com subcomponentes do Dagger
A página do documento Como usar o Dagger em apps Android explica como criar e usar
subcomponentes. No entanto, não é possível usar o mesmo código porque
os módulos de recursos não conhecem o módulo app
. Por exemplo, se você pensar
em um fluxo de login típico e no código que temos na página anterior, ele
não vai compilar mais:
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); ... } }
O motivo é que o módulo login
desconhece MyApplication
e
appComponent
. Para que isso funcione, é necessário definir uma interface no módulo de
recurso que forneça um FeatureComponent
que MyApplication
precise implementar.
No exemplo a seguir, você pode definir uma interface LoginComponentProvider
que forneça um LoginComponent
no módulo login
para o fluxo de login:
Kotlin
interface LoginComponentProvider { fun provideLoginComponent(): LoginComponent }
Java
public interface LoginComponentProvider { public LoginComponent provideLoginComponent(); }
Agora, o LoginActivity
vai usar essa interface em vez do snippet de código
definido acima:
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); ... } }
Agora, MyApplication
precisa implementar essa interface e os
métodos necessários:
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(); } }
Assim, é possível usar os subcomponentes do Dagger em um projeto multimódulo. Com os módulos de recursos, a solução é diferente devido à maneira como os módulos dependem uns dos outros.
Dependências de componentes com módulos de recursos
Com os módulos de recursos, a maneira como os módulos geralmente dependem
uns dos outros é invertida. Em vez de o módulo app
incluir módulos de
recursos, os módulos de recursos dependem do módulo app
. Veja na Figura 2
uma representação de como os módulos são estruturados.
No Dagger, os componentes precisam saber sobre os subcomponentes. Essas informações
estão incluídas em um módulo do Dagger adicionado ao componente pai (como o
módulo SubcomponentsModule
em Como usar o Dagger em apps Android).
Infelizmente, com a dependência invertida entre o app e o
módulo de recurso, o subcomponente não fica visível no módulo app
, porque
ele não está no caminho de criação. Por exemplo, um LoginComponent
definido em um
módulo de recurso login
não pode ser um subcomponente de
ApplicationComponent
definido no módulo app
.
O Dagger tem um mecanismo chamado dependências de componentes, que você pode usar para resolver esse problema. Em vez de o componente filho ser um subcomponente do componente pai, ele depende do componente pai. Com isso, não há relação pai-filho. Agora, os componentes precisam de outros para receber determinadas dependências. Os componentes precisam expor tipos do gráfico para que os componentes dependentes os consumam.
Por exemplo: um módulo de recurso chamado login
quer criar um
LoginComponent
que depende do AppComponent
disponível no
módulo app
do Gradle.
Veja abaixo as definições das classes e do AppComponent
que fazem parte do
módulo app
do 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 { ... }
No módulo login
do Gradle que inclui o módulo app
, você tem uma
LoginActivity
que precisa de uma instância LoginViewModel
para ser injetada:
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
depende do UserRepository
que está disponível e
com escopo definido no AppComponent
. Vamos criar um LoginComponent
que depende de
AppComponent
para injetar 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 uma dependência de AppComponent
adicionando-a ao
parâmetro de dependências da anotação do componente. Como LoginActivity
vai ser
injetado pelo Dagger, adicione o método inject()
à interface.
Ao criar um LoginComponent
, uma instância de AppComponent
precisa ser
transmitida. Use a fábrica de componentes para fazer isso:
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); }
Agora, LoginActivity
pode criar uma instância de LoginComponent
e chamar o
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
. Para que LoginComponent
possa acessá-lo em AppComponent
, AppComponent
precisa expô-lo
na interface:
Kotlin
@Singleton @Component interface AppComponent { fun userRepository(): UserRepository }
Java
@Singleton @Component public interface AppComponent { UserRepository userRepository(); }
As regras de escopo com componentes dependentes funcionam da mesma forma
que os subcomponentes. Como LoginComponent
usa uma instância de AppComponent
,
eles não podem usar a mesma anotação de escopo.
Se você quisesse definir o escopo de LoginViewModel
como LoginComponent
, faria como fez
antes, usando a anotação @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áticas recomendadas
O
ApplicationComponent
precisa estar sempre no móduloapp
.Crie componentes do Dagger em módulos se você precisa executar a injeção de campos nesse módulo ou se precisa criar um escopo de objetos para um fluxo específico do aplicativo.
Para módulos do Gradle que são utilitários ou auxiliares e não precisam criar um gráfico (é por isso que você precisa de um componente do Dagger), crie e exponha módulos públicos do Dagger com os métodos @Provides e @Binds dessas classes que não oferecem suporte à injeção de construtor.
Para usar o Dagger em um app Android com módulos de recursos, use as dependências do componente para poder acessar as dependências fornecidas pelo
ApplicationComponent
definido no móduloapp
.