Como usar o Dagger em apps para Android

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.
  • 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.

LoginActivity depende do LoginViewModel, que depende do UserRepository,
  que depende do UserLocalDataSource e do UserRemoteDataSource, que, por sua vez,
  depende do Retrofit.

Figura 1. Gráfico de dependência do exemplo de código.

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. Por 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:

Diagrama do gráfico de dependências LoginActivity

Figura 2. Representação do gráfico com LoginActivity sendo injetada pelo Dagger.

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:

  1. A instância de LoginViewModel permaneceria na memória após a conclusão do fluxo.

  2. 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 de LoginViewModel, 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:

  1. Criar um novo módulo do Dagger (por exemplo, SubcomponentsModule) transmitindo a classe do subcomponente para o atributo subcomponents 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 {
    }
    
  2. Adicionando o novo módulo (por exemplo, SubcomponentsModule) a ApplicationComponent:

    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 injetar LoginActivity porque essa responsabilidade agora pertence a LoginComponent. Portanto, você pode remover o método inject() de ApplicationComponent.

    Os consumidores de ApplicationComponent precisam saber como criar instâncias de LoginComponent. 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:

  3. 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 @ActivityScopeem 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.

Gráfico do app depois de adicionar o último subcomponente

Figura 3. Representação do gráfico que você criou para o exemplo de app Android.

Vamos detalhar as partes do gráfico:

  1. O NetworkModule (e, portanto, LoginRetrofitService) está incluído no ApplicationComponent porque você o especificou no componente.

  2. UserRepository permanece em ApplicationComponent porque está no escopo de ApplicationComponent. Se o projeto crescer, é uma boa ideia compartilhar a mesma instância em recursos diferentes (por exemplo, no Registro).

    Como UserRepository faz parte de ApplicationComponent, as dependências (por exemplo, UserLocalDataSource e UserRemoteDataSource) também precisam estar nesse componente para fornecer instâncias de UserRepository.

  3. LoginViewModel está incluído em LoginComponent porque é exigido apenas pelas classes injetadas por LoginComponent. LoginViewModel não está incluído em ApplicationComponent porque nenhuma dependência em ApplicationComponent precisa de LoginViewModel.

    Da mesma forma, se você não tivesse o escopo UserRepository para ApplicationComponent, o Dagger teria incluído automaticamente UserRepository e as dependências dele como parte de LoginComponent, porque esse é o único lugar em que UserRepository é 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 por ApplicationComponent, e LoginActivity é responsável por LoginComponent.

  • 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:

  1. Refatore os módulos e extraia o módulo comum para o componente.
  2. 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.