Utiliser Dagger dans des applications multimodules

Un projet comportant plusieurs modules Gradle est appelé "projet multimodule". Dans un projet multimodule envoyé en tant qu'APK unique sans module de fonctionnalité, il est courant de disposer d'un module app pouvant dépendre de la plupart des modules de votre projet, ainsi que d'un module base ou core dont dépendent les autres modules. Le module app contient généralement votre classe Application, tandis que le module base contient toutes les classes communes à tous les modules de votre projet.

Le module app est le bon endroit pour afficher votre composant d'application (par exemple, ApplicationComponent dans l'image ci-dessous) qui peut fournir des objets dont d'autres composants pourraient avoir besoin, ainsi que les Singletons de votre application. Par exemple, les classes telles que OkHttpClient, les analyseurs JSON, les accesseurs de votre base de données ou les objets SharedPreferences qui peuvent être définis dans le module core seront fournis par l'élément ApplicationComponent défini dans le module app.

Dans le module app, d'autres composants peuvent également avoir une durée de vie plus courte. Par exemple, une propriété UserComponent avec une configuration spécifique à l'utilisateur (comme UserSession) après une connexion.

Dans les différents modules de votre projet, vous pouvez définir au moins un sous-composant ayant une logique spécifique à ce module, comme l'illustre l'image 1.

Image 1. Exemple de graphique Dagger dans un projet multimodule

Par exemple, dans un module login, vous pouvez avoir un élément LoginComponent limité par une annotation @ModuleScope personnalisée pouvant fournir des objets communs à cette fonctionnalité, comme un élément LoginRepository. Dans ce module, d'autres composants peuvent aussi dépendre d'un élément LoginComponent avec un champ d'application personnalisé, par exemple @FeatureScope pour LoginActivityComponent, ou d'un élément TermsAndConditionsComponent dans lequel vous pouvez appliquer une logique spécifique à une caractéristique, comme des objets ViewModel.

Pour les autres modules tels que Registration, la configuration est semblable.

En règle générale, pour un projet multimodule, les modules de même niveau ne doivent pas dépendre les uns des autres. Si c'est le cas, déterminez si cette logique partagée (les dépendances entre elles) doit faire partie du module parent. Procédez alors à une refactorisation pour déplacer les classes vers le module parent. Si ce n'est pas le cas, créez un nouveau module qui étend le module parent et demandez aux deux modules d'origine d'étendre le nouveau module.

Il est recommandé de créer un composant dans un module dans les cas suivants :

  • Vous devez injecter un champ, comme avec LoginActivityComponent.

  • Vous devez définir le champ d'application des objets, comme avec LoginComponent.

Si aucun de ces cas ne s'applique et que vous devez indiquer à Dagger comment fournir des objets de ce module, créez et affichez un module Dagger avec des méthodes @Provides ou @Binds si l'injection de construction n'est pas possible pour ces classes.

Intégration de sous-composants Dagger

La page de documentation Utiliser Dagger dans les applications Android explique comment créer et utiliser des sous-composants. Toutefois, vous ne pouvez pas utiliser le même code, car les modules de fonctionnalité ne connaissent pas le module app. Par exemple, si vous pensez à un flux de connexion type et au code de la page précédente, la compilation s'arrête :

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);

        ...
    }
}

La raison est que le module login ne connaît ni MyApplication, ni appComponent. Pour y arriver, vous devez définir une interface dans le module de fonctionnalité qui fournit un élément FeatureComponent que MyApplication doit intégrer.

Dans l'exemple suivant, vous pouvez définir une interface LoginComponentProvider qui fournit un élément LoginComponent dans le module login du flux de connexion :

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

À l'avenir, LoginActivity utilisera cette interface au lieu de l'extrait de code défini ci-dessus :

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);

        ...
    }
}

MyApplication doit maintenant intégrer cette interface et les méthodes requises :

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();
  }
}

Voici comment utiliser des sous-composants Dagger dans un projet multimodule. Avec les modules de fonctionnalité, la solution est différente, car les modules dépendent les uns des autres.

Dépendances de composants avec des modules de fonctionnalité

Avec des modules de fonctionnalité, la manière dont les modules dépendent généralement les uns des autres est inversée. Au lieu du module app incluant des modules de fonctionnalité, ceux-ci dépendent du module app. L'image 2 illustre la structure des modules.

Image 2. Exemple de graphique Dagger dans un projet avec des modules de fonctionnalité

Dans Dagger, les composants doivent connaître leurs sous-composants. Ces informations sont incluses dans un module Dagger ajouté au composant parent (comme le module SubcomponentsModule dans la section Utiliser Dagger dans des applications Android).

Malheureusement, avec la dépendance inverse entre l'application et le module de fonctionnalité, le sous-composant n'est pas visible depuis le module app, car il ne se trouve pas dans le chemin de compilation. Par exemple, un élément LoginComponent défini dans un module de fonctionnalité login ne peut pas être un sous-composant de l'élément ApplicationComponent défini dans le module app.

Dagger dispose d'un mécanisme appelé dépendances de composants qui permet de résoudre ce problème. Au lieu d'être un sous-composant du composant parent, le composant enfant dépend du composant parent. Il n'y a donc pas de relation parent-enfant : les composants dépendent d'autres éléments pour obtenir certaines dépendances. Les composants doivent exposer les types du graphique pour que les composants dépendants les consomment.

Par exemple, un module de fonctionnalité appelé login souhaite créer un LoginComponent qui dépend de l'élément AppComponent disponible dans le module Gradle app.

Vous trouverez ci-dessous les définitions des classes et l'élément AppComponent qui font partie du module Gradle app :

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 { ... }

Dans votre module Gradle login, qui inclut le module app, vous disposez d'un élément LoginActivity nécessitant l'injection d'une instance LoginViewModel :

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

LoginViewModel dispose d'une dépendance vis-à-vis de UserRepository, disponible et limitée à AppComponent. Créons un élément LoginComponent qui dépend de l'élément AppComponent pour injecter 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 spécifie une dépendance sur AppComponent en l'ajoutant au paramètre de dépendances de l'annotation du composant. Étant donné que LoginActivity sera injecté par Dagger, ajoutez la méthode inject() à l'interface.

Lors de la création d'un élément LoginComponent, une instance de l'élément AppComponent doit être transmise. Pour ce faire, utilisez la fabrique de composants :

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);
}

LoginActivity peut maintenant créer une instance de LoginComponent et appeler la méthode 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 dépend de UserRepository. Pour que LoginComponent puisse y accéder à partir de l'élément AppComponent, AppComponent doit l'exposer dans son interface :

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

Les règles de champ d'application avec des composants dépendants fonctionnent de la même manière qu'avec des sous-composants. Comme LoginComponent utilise une instance de l'élément AppComponent, il ne peut pas utiliser la même annotation de champ d'application.

Si vous souhaitez limiter LoginViewModel à LoginComponent, procédez comme auparavant avec l'annotation personnalisée @ActivityScope.

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;
    }
}

Bonnes pratiques

  • ApplicationComponent doit toujours se trouver dans le module app.

  • Créez des composants Dagger dans des modules si vous devez effectuer une injection de champs dans ce module ou si vous devez limiter les objets à un flux spécifique de votre application.

  • Pour les modules Gradle destinés à être des utilitaires ou des assistants et qui n'ont pas besoin de créer de graphique (ce qui explique la nécessité d'avoir un composant Dagger), créez et exposez des modules Dagger publics avec les méthodes @Provides et @Binds de ces classes qui ne sont pas compatibles avec l'injection de constructeurs.

  • Pour utiliser Dagger dans une application Android avec des modules de fonctionnalité, utilisez des dépendances de composants afin d'accéder aux dépendances fournies par l'élément ApplicationComponent défini dans le module app.