Como usar o Dagger em apps multimódulo

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.

Figura 1. Exemplo de um gráfico do Dagger em um projeto multimódulo.

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.

Figura 2. Exemplo de um gráfico do Dagger em um projeto com módulos de recursos.

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ódulo app.

  • 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ódulo app.