A página Princípios básicos do Dagger explica como o Dagger pode ajudar a automatizar a injeção de dependências no seu app. Com o Dagger, você não precisa programar códigos boilerplate tediosos e propensos a erros.
Resumo das práticas recomendadas
- Use a injeção de construtor com
@Inject
para adicionar tipos ao gráfico do Dagger quando possível. Quando não for possível:- Use
@Binds
para informar ao Dagger qual implementação uma interface precisa ter. - Use
@Provides
para dizer ao Dagger como fornecer classes que seu projeto não tem.
- Use
- Só é preciso declarar módulos uma vez em um componente.
- Nomeie as anotações de escopo dependendo do ciclo de vida em que a
anotação é usada. Os exemplos incluem
@ApplicationScope
,@LoggedUserScope
e@ActivityScope
.
Como adicionar dependências
Para usar o Dagger no seu projeto, adicione estas dependências ao seu app
no arquivo build.gradle
. Você pode encontrar a versão mais recente do Dagger
neste projeto do GitHub (link em inglês).
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' }
O Dagger no Android
Considere um exemplo de app Android com o gráfico de dependência da Figura 1.
Normalmente, no Android, você cria um gráfico do Dagger que fica na classe
do seu app porque você quer que uma instância do gráfico fique na memória
enquanto o app estiver em execução. Dessa forma, o gráfico é anexado ao ciclo de vida do app. Em alguns
casos, pode ser necessário disponibilizar o contexto do app no
gráfico. Para isso, o gráfico precisa estar na
classe Application
. Uma vantagem dessa
abordagem é que o gráfico fica disponível para outras classes do framework do Android.
Além disso, ele simplifica os testes, permitindo que você use
uma classe Application
personalizada.
Como a interface que gera o gráfico é anotada com @Component
,
você pode dar a ela o nome ApplicationComponent
ou ApplicationGraph
. Geralmente, você mantém
uma instância desse componente na classe Application
personalizada e a chama
sempre que precisa do gráfico do aplicativo, conforme mostrado no snippet de código
a seguir.
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(); }
Como algumas classes do framework do Android (por exemplo atividades e fragmentos) são
instanciadas pelo sistema, o Dagger não pode criá-las para você. Para atividades
específicas, qualquer código de inicialização precisa acessar o método onCreate()
.
Isso significa que não é possível usar a anotação @Inject
no construtor da
classe (injeção de construtor), como feito nos exemplos anteriores. Em vez disso,
é necessário usar a injeção de campo.
Em vez de criar as dependências exigidas por uma atividade no método onCreate()
,
você quer que o Dagger preencha essas dependências para você. Para injeção
de campo, aplique a anotação @Inject
aos campos que
quer do gráfico do 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
não é um ViewModel dos Componentes da arquitetura do
Android. Ele é apenas uma classe normal atuando como um ViewModel.
Para ver mais informações sobre como injetar essas classes, confira o código
na implementação oficial Android Blueprints Dagger, na
ramificação dev-dagger (links em inglês).
Uma das considerações sobre o Dagger é que os campos injetados não podem ser particulares. É necessário ter pelo menos a visibilidade privada do pacote, como no código anterior.
Como injetar atividades
O Dagger precisa saber se LoginActivity
tem que acessar o gráfico para
poder disponibilizar o ViewModel
necessário. Na página Princípios básicos do Dagger, você usou
a interface @Component
para receber objetos do gráfico
ao expor funções com o tipo de retorno daquilo que quer receber do
gráfico. Nesse caso, é preciso informar ao Dagger sobre um objeto (LoginActivity
neste caso) que exige a injeção de uma dependência. Para isso, exponha
uma função que tome como parâmetro o objeto que solicita a injeção.
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); }
Essa função informa ao Dagger que LoginActivity
quer acessar o gráfico e
solicita a injeção. O Dagger precisa satisfazer a todas as dependências que
LoginActivity
precisa (LoginViewModel
com as próprias dependências).
Se você tiver várias classes que solicitam injeção, precisará declarar todas elas
de maneira específica no componente com o tipo exato delas. Por exemplo, se você tiver
LoginActivity
e RegistrationActivity
solicitando uma injeção, terá dois métodos
inject()
em vez de um genérico que abrange os dois casos. Um método
genérico inject()
não diz ao Dagger o que precisa ser disponibilizado. As funções
na interface podem ter qualquer nome, mas é uma convenção no Dagger chamá-las de inject()
quando
recebem o objeto que vai ser injetado como um parâmetro.
Para injetar um objeto na atividade, você precisa usar o appComponent
definido na
sua classe Application
e chamar o método inject()
, transmitindo uma instância da
atividade que solicita a injeção.
Ao usar as atividades, injete o Dagger no
método onCreate()
da atividade antes de chamar super.onCreate()
para evitar problemas
com a restauração de fragmentos. Durante a fase de restauração em super.onCreate()
,
uma atividade anexa fragmentos que podem querer acessar as vinculações de atividades.
Ao usar fragmentos, injete o Dagger no método onAttach()
do fragmento. Nesse caso, isso pode ser feito antes ou depois de chamar 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; } }
Vamos dizer ao Dagger como fornecer o restante das dependências para criar o gráfico:
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 do Dagger
Neste exemplo, você vai usar a biblioteca de rede Retrofit.
UserRemoteDataSource
depende de LoginRetrofitService
. No entanto,
a maneira de criar uma instância de LoginRetrofitService
é diferente do
que você fez até agora. Não é uma instanciação de classe. É o resultado de
chamar Retrofit.Builder()
e transmitir parâmetros diferentes para configurar
o serviço de login.
Além da anotação @Inject
, há uma outra maneira de dizer ao Dagger como
fornecer uma instância de uma classe: as informações dentro dos módulos do Dagger. Um módulo
do Dagger é uma classe anotada com @Module
. Lá, você pode definir
dependências com a anotação @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); } }
Os módulos são uma forma de encapsular semanticamente as informações sobre como fornecer
objetos. Como você pode notar, você chamou a classe NetworkModule
para agrupar a lógica
de fornecer objetos relacionados à rede. Se o app se expandir, você também
poderá adicionar como fornecer um OkHttpClient
aqui ou como
configurar o Gson ou Moshi.
As dependências de um método @Provides
são os parâmetros desse método. Para o
método anterior, LoginRetrofitService
pode ser fornecido sem dependências
porque o método não tem parâmetros. Se você declarasse OkHttpClient
como
parâmetro, o Dagger precisaria fornecer uma instância OkHttpClient
do
gráfico para satisfazer às dependências de LoginRetrofitService
. Exemplo:
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 o gráfico do Dagger saiba sobre esse módulo, você precisa adicioná-lo à
interface @Component
da seguinte maneira:
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 { ... }
A maneira recomendada de adicionar tipos ao gráfico Dagger é com a injeção
de construtor (isto é, com a anotação @Inject
no construtor da classe).
Às vezes, isso não é possível e é necessário usar os módulos do Dagger. Um exemplo
é quando você quer que o Dagger use o resultado de um cálculo para determinar como
criar uma instância de um objeto. Sempre que precisar fornecer uma instância desse
tipo, o Dagger executará o código dentro do método @Provides
.
É assim que o gráfico do Dagger aparece no exemplo:
O ponto de entrada para o gráfico é LoginActivity
. Como LoginActivity
injeta
LoginViewModel
, o Dagger cria um gráfico que sabe fornecer uma instância
de LoginViewModel
e, de maneira recursiva, das dependências. Ele sabe fazer isso devido à anotação @Inject
no construtor das classes.
Dentro do ApplicationComponent
gerado pelo Dagger, há um método de
fábrica para receber instâncias de todas as classes que ele sabe fornecer. Nesse
exemplo, o Dagger delega para NetworkModule
incluído em
ApplicationComponent
para conseguir uma instância de LoginRetrofitService
.
Escopos do Dagger
Os escopos são mencionados na página Princípios básicos do Dagger como uma forma de ter uma instância exclusiva de um tipo em um componente. Isso é o que significa selecionar um tipo para o ciclo de vida do componente.
Como você pode usar UserRepository
em outros recursos do app e
não querer criar um novo objeto sempre que precisar dele, é possível designá-lo
como uma instância exclusiva para todo o app. O mesmo ocorre com
LoginRetrofitService
, já que você também precisará que uma instância exclusiva desse objeto seja reutilizada e a criação dele pode ser cara. Criar uma instância de
UserRemoteDataSource
não é tão caro. Portanto, não é necessário definir o escopo para o
ciclo de vida do componente.
@Singleton
é a única anotação de escopo que vem com
o pacote javax.inject
. É possível usá-la para anotar ApplicationComponent
e os objetos que você quer reutilizar em todo o app.
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() { ... } }
Tenha cuidado para não introduzir vazamentos de memória ao aplicar escopos a objetos. Contanto
que o componente com escopo esteja na memória, o objeto criado também
vai estar. Como ApplicationComponent
é criado quando o app é iniciado (na
classe Application
), ele é destruído quando o app é destruído. Assim, a
instância exclusiva de UserRepository
sempre permanece na memória até
que o aplicativo seja destruído.
Subcomponentes do Dagger
Se o fluxo de login (gerenciado por um único LoginActivity
) consistir em vários
fragmentos, reutilize a mesma instância de LoginViewModel
em todos os
fragmentos. Não é possível anotar LoginViewModel
com @Singleton
para reutilizar a instância
pelos seguintes motivos:
A instância de
LoginViewModel
permaneceria na memória após a conclusão do fluxo.Você quer uma instância diferente de
LoginViewModel
para cada fluxo de login. Por exemplo, se o usuário fizer logout, convém ter uma instância diferente deLoginViewModel
, em vez da mesma instância de quando o usuário fez login pela primeira vez.
Para definir o escopo LoginViewModel
para o ciclo de vida de LoginActivity
, é necessário criar
um novo componente (um novo subgráfico) para o fluxo de login e um novo escopo.
Vamos criar um gráfico específico para o fluxo de login.
Kotlin
@Component interface LoginComponent {}
Java
@Component public interface LoginComponent { }
Agora, LoginActivity
precisa receber injeções de LoginComponent
porque
tem uma configuração específica de login. Isso remove a responsabilidade de injetar
LoginActivity
da classe ApplicationComponent
.
Kotlin
@Component interface LoginComponent { fun inject(activity: LoginActivity) }
Java
@Component public interface LoginComponent { void inject(LoginActivity loginActivity); }
LoginComponent
precisa ser capaz de acessar os objetos de ApplicationComponent
porque LoginViewModel
depende de UserRepository
. A maneira de dizer ao Dagger que
você quer um novo componente para usar parte de outro é com os
subcomponentes do Dagger. O novo componente precisa ser um subcomponente do
componente que contém recursos compartilhados.
Os subcomponentes herdam e estendem o gráfico de objetos de um componente pai. Assim, todos os objetos fornecidos no componente pai também são fornecidos no subcomponente. Dessa forma, um objeto de um subcomponente pode depender de um objeto fornecido pelo componente pai.
Para criar instâncias de subcomponentes, você precisa de uma instância do componente pai. Portanto, os objetos fornecidos pelo componente pai ao subcomponente continuam restritos ao componente pai.
No exemplo, você precisa definir LoginComponent
como um subcomponente de
ApplicationComponent
. Para fazer isso, anote LoginComponent
com
@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); }
Também é preciso definir uma fábrica de subcomponentes dentro de LoginComponent
para que
ApplicationComponent
saiba como criar instâncias 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 informar ao Dagger que LoginComponent
é um subcomponente de
ApplicationComponent
, você precisa indicá-lo da seguinte forma:
Criar um novo módulo do Dagger (por exemplo,
SubcomponentsModule
) transmitindo a classe do subcomponente para o atributosubcomponents
da anotação.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 { }
Adicionando o novo módulo (por exemplo,
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 { }
Observe que
ApplicationComponent
não precisa mais injetarLoginActivity
porque essa responsabilidade agora pertence aLoginComponent
. Portanto, você pode remover o métodoinject()
deApplicationComponent
.Os consumidores de
ApplicationComponent
precisam saber como criar instâncias deLoginComponent
. O componente pai precisa adicionar um método na interface para permitir que os consumidores criem instâncias do subcomponente a partir de uma instância dele:Exponha a fábrica que cria instâncias de
LoginComponent
na interface: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(); }
Como atribuir escopos a subcomponentes
Se você criar o projeto, poderá criar instâncias de ApplicationComponent
e LoginComponent
. ApplicationComponent
é anexado ao ciclo de vida do
aplicativo porque é interessante usar a mesma instância do gráfico, desde que o
aplicativo esteja na memória.
Qual é o ciclo de vida de LoginComponent
? Um dos motivos para precisar de
LoginComponent
é porque era necessário compartilhar a mesma instância de
LoginViewModel
entre fragmentos relacionados ao login. Além disso, é interessante ter instâncias
diferentes de LoginViewModel
sempre que tiver um novo fluxo de login. LoginActivity
é o ciclo de vida certo para LoginComponent
: a cada nova atividade, você precisa
de uma nova instância de LoginComponent
e de fragmentos que possam usar essa instância de
LoginComponent
.
Como LoginComponent
está anexado ao ciclo de vida de LoginActivity
, você precisa
manter uma referência ao componente na atividade da mesma forma que a
referência ao applicationComponent
na classe Application
. Dessa forma,
os fragmentos podem acessá-lo.
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; ... }
Observe que a variável loginComponent
não é anotada com @Inject
porque não é esperado que essa variável seja fornecida pelo Dagger.
Você pode usar ApplicationComponent
para ter uma referência a LoginComponent
e depois injetar LoginActivity
da seguinte maneira:
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); } }
LoginComponent
é criado no método onCreate()
da atividade e será
destruído de maneira implícita quando a atividade for destruída.
O LoginComponent
precisa sempre fornecer a mesma instância de LoginViewModel
sempre que for solicitado. Para garantir isso, crie um escopo de anotação personalizado
e anote LoginComponent
e LoginViewModel
com ele. Não
é possível usar a anotação @Singleton
porque ela já foi usada
pelo componente pai e isso tornaria o objeto um app singleton
(instância exclusiva para todo o app). É necessário criar um escopo de anotação
diferente.
Nesse caso, você poderia ter chamado esse escopo de @LoginScope
, mas essa não é uma prática
recomendada. O nome da anotação de escopo não precisa ser explícito para a finalidade que ela
atende. Em vez disso, ela precisa ser nomeada dependendo da vida útil, porque
as anotações podem ser reutilizadas por componentes semelhantes, como RegistrationComponent
e SettingsComponent
. É por isso que é melhor chamá-la de @ActivityScope
em vez
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; } }
Agora, se você tinha dois fragmentos que precisam de LoginViewModel
, ambos são
fornecidos com a mesma instância. Por exemplo, se você tiver
um LoginUsernameFragment
e um LoginPasswordFragment
, eles precisarão ser injetados
pelo 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); }
Os componentes acessam a instância do componente que reside no
objeto LoginActivity
. O código de exemplo para LoginUserNameFragment
aparece no
snippet a seguir.
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) } }
E o mesmo para 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) } }
A Figura 3 mostra como o gráfico do Dagger fica com o novo subcomponente. As classes
com um ponto branco (UserRepository
, LoginRetrofitService
e LoginViewModel
)
são aquelas que têm uma instância exclusiva com escopo para os respectivos componentes.
Vamos detalhar as partes do gráfico:
O
NetworkModule
(e, portanto,LoginRetrofitService
) está incluído noApplicationComponent
porque você o especificou no componente.UserRepository
permanece emApplicationComponent
porque está no escopo deApplicationComponent
. Se o projeto crescer, é uma boa ideia compartilhar a mesma instância em recursos diferentes (por exemplo, no Registro).Como
UserRepository
faz parte deApplicationComponent
, as dependências (por exemplo,UserLocalDataSource
eUserRemoteDataSource
) também precisam estar nesse componente para fornecer instâncias deUserRepository
.LoginViewModel
está incluído emLoginComponent
porque é exigido apenas pelas classes injetadas porLoginComponent
.LoginViewModel
não está incluído emApplicationComponent
porque nenhuma dependência emApplicationComponent
precisa deLoginViewModel
.Da mesma forma, se você não tivesse o escopo
UserRepository
paraApplicationComponent
, o Dagger teria incluído automaticamenteUserRepository
e as dependências dele como parte deLoginComponent
, porque esse é o único lugar em queUserRepository
é usado.
Além de criar escopos de objetos para um ciclo de vida diferente, é recomendado criar subcomponentes para encapsular partes diferentes de seu aplicativo para que fiquem separadas umas das outras.
Estruturar seu app para criar diferentes subgráficos do Dagger, dependendo do fluxo do app, ajuda a criar um aplicativo mais eficiente e escalonável em termos de memória e tempo de inicialização.
Práticas recomendadas ao criar um gráfico do Dagger
Ao criar o gráfico do Dagger para seu app:
Ao criar um componente, é necessário considerar qual elemento é responsável pelo ciclo de vida dele. Nesse caso, a classe
Application
é responsável porApplicationComponent
, eLoginActivity
é responsável porLoginComponent
.Use o escopo só quando fizer sentido. O uso excessivo do escopo pode ter um efeito negativo no desempenho do tempo de execução do app: o objeto fica na memória enquanto o componente estiver também e ter um objeto com escopo custa mais caro. Quando o Dagger fornece o objeto, ele usa o bloqueio
DoubleCheck
em vez de um provedor de tipo de fábrica.
Como testar um projeto que usa o Dagger
Um dos benefícios de usar estruturas de injeção de dependência como o Dagger é que ele facilita o teste do código.
Testes de unidades
Você não precisa usar o Dagger para testes de unidades. Ao testar uma classe que usa a injeção de construtor, você não precisa usar o Dagger para instanciar essa classe. Você pode chamar diretamente seu construtor, transmitindo dependências falsas ou fictícias de forma direta da mesma forma que faria se elas não estivessem anotadas.
Por exemplo, ao testar 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(...); } }
Testes de ponta a ponta
Para testes de integração, uma prática recomendada é criar um
TestApplicationComponent
destinado a testes.
A produção e os testes usam uma configuração de componente diferente.
Isso exige um design dos módulos mais direto no seu aplicativo. O componente de teste estende o componente de produção e instala um 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
tem uma implementação falsa do NetworkModule
original.
Lá você pode fornecer instâncias falsas ou simulações do que quer substituir.
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(); } }
Nos testes de integração ou nos de ponta a ponta, use um TestApplication
que
cria TestApplicationComponent
em vez de um 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(); }
Em seguida, esse app de teste é usado em um TestRunner
personalizado para
executar testes de instrumentação. Para mais informações, consulte o codelab Como usar
o Dagger no seu app Android.
Como trabalhar com módulos do Dagger
Os módulos do Dagger são uma maneira de encapsular como fornecer objetos de maneira semântica. Você pode incluir módulos em componentes, mas também pode incluí-los dentro de outros módulos. Isso é eficiente, mas pode ser usado de forma indevida.
Depois que um módulo é adicionado a um componente ou a outro módulo, ele já está no gráfico do Dagger, que pode fornecer tais objetos nesse componente. Antes de adicionar um módulo, verifique se ele já faz parte do gráfico do Dagger, confirmando se ele já foi adicionado ao componente ou compilando o projeto. Verifique também se o Dagger consegue encontrar as dependências necessárias para esse módulo.
A prática recomendada determina que os módulos só precisam ser declarados uma vez em um componente (fora dos casos de uso avançados do Dagger).
Digamos que você tenha configurado seu gráfico dessa maneira. ApplicationComponent
inclui Module1
e Module2
, e Module1
inclui 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 { ... }
Se, agora, Module2
depende das classes fornecidas por ModuleX
. Uma prática negativa
é incluir ModuleX
em Module2
, porque ModuleX
será incluído duas vezes no
gráfico, como visto no snippet de código a seguir.
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 { ... }
Em vez disso, siga um destes procedimentos:
- Refatore os módulos e extraia o módulo comum para o componente.
- Crie um novo módulo com os objetos que ambos compartilham e extraia-o para o componente.
Não refatorar dessa maneira resulta em muitos módulos incluindo uns aos outros, sem um sentido claro de organização e dificultando a visualização da origem de cada dependência.
Prática recomendada (Opção 1): o ModuleX é declarado uma vez no gráfico do 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 { ... }
Prática recomendada (opção 2): dependências comuns de Module1
e Module2
em ModuleX
são extraídas para um novo módulo, chamado ModuleXCommon
, que está
incluído no componente. Em seguida, dois outros módulos chamados
ModuleXWithModule1Dependencies
e ModuleXWithModule2Dependencies
são
criados com as dependências específicas de cada um deles. Todos eles
são declarados uma vez no gráfico do 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 { ... }
Injeção assistida
Injeção assistida é um padrão de DI usado para construir um objeto em que alguns parâmetros podem ser fornecidos pelo framework da DI e outros precisam ser transmitidos no momento da criação pelo usuário.
No Android, esse padrão é comum em telas de detalhes em que o ID do elemento que vai ser mostrado é conhecido apenas no momento da execução, não no da compilação, quando o Dagger gera o gráfico da DI. Para saber mais sobre injeção assistida com o Dagger, consulte a documentação do Dagger.
Conclusão
Caso você ainda não tenha feito isso, consulte a seção de práticas recomendadas. Consulte o codelab Como usar o Dagger em um app Android para saber mais detalhes.