Utiliser Dagger dans des applications Android

La page Principes de base de Dagger explique comment Dagger peut vous aider à rendre l'injection de dépendances automatique dans votre application. Avec Dagger, vous n'avez pas à écrire de code récurrent fastidieux et sujet aux erreurs.

Résumé des bonnes pratiques

  • Utilisez l'injection par constructeur avec @Inject pour ajouter des types au graphique Dagger chaque fois que cela est possible. Dans le cas contraire :
    • Utilisez @Binds pour indiquer à Dagger quelle intégration devrait avoir une interface.
    • Utilisez @Provides pour indiquer à Dagger comment fournir des classes dont votre projet n'est pas propriétaire.
  • Vous ne devez déclarer les modules qu'une seule fois dans un composant.
  • Attribuez un nom aux annotations de champ d'application en fonction de leur durée de vie. Il peut par exemple s'agir de @ApplicationScope, @LoggedUserScope et @ActivityScope.

Ajouter des dépendances

Pour utiliser Dagger dans votre projet, ajoutez ces dépendances à votre application dans votre fichier build.gradle. Vous trouverez la dernière version de Dagger dans ce projet GitHub.

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

Dagger avec Android

Prenons l'exemple d'une application Android avec le graphique de dépendances représenté sur l'image 1.

LoginActivity dépend de LoginViewModel, qui dépend de UserRepository, qui dépend de UserLocalDataSource et de UserRemoteDataSource, qui à son tour dépend de Retrofit.

Figure 1. Graphique de dépendances du code d'exemple

Dans Android, vous créez généralement un graphique Dagger qui réside dans votre classe d'application, car vous souhaitez qu'une instance du graphique reste en mémoire tant que l'application est en cours d'exécution. De cette manière, le graphique est associé au cycle de vie de l'application. Vous pouvez parfois vouloir que le contexte de l'application soit également disponible dans le graphique. Pour cela, le graphique doit également se trouver dans la classe Application. L'un des avantages de cette approche est que le graphique est disponible pour d'autres classes du framework Android. De plus, cela simplifie les tests en vous permettant d'utiliser une classe Application personnalisée pour les effectuer.

Comme l'interface qui génère le graphique est annotée avec @Component, vous pouvez l'appeler ApplicationComponent ou ApplicationGraph. Vous conservez en général une instance de ce composant dans votre classe Application personnalisée et l'appelez chaque fois que vous avez besoin du graphique d'application, comme indiqué dans l'extrait de code suivant :

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

Étant donné que certaines classes du framework Android, comme les activités et les fragments, sont instanciées par le système, Dagger ne peut pas les créer pour vous. Pour les activités en particulier, tout code d'initialisation doit passer par la méthode onCreate(). Cela signifie que vous ne pouvez pas utiliser l'annotation @Inject dans le constructeur de la classe (injection par constructeur) comme vous avez pu le faire dans les exemples précédents. Vous devez plutôt utiliser l'injection par champs.

Au lieu de créer les dépendances requises par une activité dans la méthode onCreate(), vous souhaitez que Dagger le fasse pour vous. Pour injecter des champs, appliquez plutôt l'annotation @Inject aux champs que vous souhaitez obtenir à partir du graphique 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;
}

Par souci de simplicité, LoginViewModel n'est pas un composant ViewModel d'architecture Android. Il s'agit d'une classe standard qui agit comme un ViewModel. Pour en savoir plus sur la manière d'injecter ces classes, consultez le code de l'intégration officielle de Dagger dans Android Blueprints dans la section dev-dagger.

L'une des contreparties de Dagger est que les champs injectés ne peuvent être privés. Ils doivent au moins disposer d'une visibilité "privée pour le paquet", comme dans le code précédent.

Activités d'injection

Dagger doit savoir que LoginActivity doit accéder au graphique pour fournir l'élément ViewModel dont il a besoin. Sur la page Principes de base de Dagger, vous avez utilisé l'interface @Component pour obtenir des objets du graphique en affichant des fonctions contenant le type renvoyé de ce que vous souhaitez obtenir du graphique. Dans ce cas, vous devez indiquer à Dagger un objet (ici, LoginActivity) qui nécessite l'injection d'une dépendance. Pour ce faire, affichez une fonction qui utilise l'objet nécessitant une injection comme paramètre.

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

Cette fonction indique à Dagger que LoginActivity souhaite accéder au graphique et demande une injection. Dagger doit satisfaire toutes les dépendances requises par LoginActivity (LoginViewModel avec ses propres dépendances). Si plusieurs classes demandent une injection, vous devez les déclarer spécifiquement dans le composant avec leur type exact. Par exemple, si LoginActivity et RegistrationActivity demandent une injection, vous disposez de deux méthodes inject() au lieu d'une méthode générique couvrant les deux cas. Une méthode inject() générique n'indique pas à Dagger ce qui doit être fourni. Les fonctions de l'interface peuvent porter n'importe quel nom, mais les appeler inject() lorsqu'elles reçoivent l'objet à injecter en tant que paramètre est une convention dans Dagger.

Pour injecter un objet dans l'activité, vous devez utiliser l'élément appComponent défini dans votre classe Application et appeler la méthode inject(), en transmettant une instance de l'activité qui demande une injection.

Lorsque vous utilisez des activités, injectez Dagger dans la méthode onCreate() de l'activité avant d'appeler super.onCreate() pour éviter les problèmes de restauration des fragments. Durant la phase de restauration dans super.onCreate(), une activité associe des fragments susceptibles de vouloir accéder aux liaisons d'activité.

Lorsque vous utilisez des fragments, injectez Dagger dans la méthode onAttach() du fragment. Dans ce cas, vous pouvez le faire avant ou après avoir appelé 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;
    }
}

Précisons à Dagger comment fournir les autres dépendances pour créer le graphique :

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

Modules Dagger

Dans cet exemple, vous utilisez la bibliothèque réseau Retrofit. UserRemoteDataSource a une dépendance par rapport à LoginRetrofitService. Cependant, la manière de créer une instance de LoginRetrofitService est différente de ce que vous faisiez jusque-là. Il ne s'agit pas d'une instanciation de classe, mais du résultat de l'appel de Retrofit.Builder() et de la transmission de différents paramètres pour configurer le service de connexion.

Outre l'annotation @Inject, il existe un autre moyen d'indiquer à Dagger comment fournir une instance d'une classe : les informations contenues dans les modules Dagger. Un module Dagger est une classe annotée avec @Module. Vous pouvez alors définir des dépendances avec l'annotation @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);
    }
}

Les modules permettent d'encapsuler sémantiquement des informations concernant la manière de fournir des objets. Comme vous pouvez le constater, vous avez appelé la classe NetworkModule pour regrouper la logique utilisée pour fournir des objets liés à la mise en réseau. Si l'application se développe, vous pouvez aussi ajouter ici comment fournir un élément OkHttpClient ou comment configurer Gson ou Moshi.

Les dépendances d'une méthode @Provides sont les paramètres de cette méthode. Pour la méthode précédente, qui ne comporte aucun paramètre, LoginRetrofitService peut être fourni sans dépendances. Si vous aviez déclaré un élément OkHttpClient comme paramètre, Dagger devrait fournir une instance OkHttpClient à partir du graphique pour prendre en compte les dépendances de LoginRetrofitService. Par exemple :

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

Pour que le graphique Dagger connaisse ce module, vous devez l'ajouter à l'interface @Component comme ceci :

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

La méthode recommandée pour ajouter des types au graphique Dagger est d'utiliser l'injection par constructeur (par exemple, avec l'annotation @Inject sur le constructeur de la classe). Il arrive que ce ne soit pas possible ; vous devez alors utiliser des modules Dagger. C'est par exemple le cas lorsque vous souhaitez que Dagger utilise le résultat d'un calcul pour déterminer comment créer une instance d'objet. Chaque fois qu'il doit fournir une instance de ce type, Dagger exécute le code dans la méthode @Provides.

Voici à quoi ressemble actuellement le graphique Dagger de cet exemple :

Schéma du graphique de dépendance LoginActivity

Figure 2. Représentation du graphique avec injection de LoginActivity par Dagger

Le point d'entrée du graphique est LoginActivity. Étant donné que LoginActivity injecte LoginViewModel, Dagger crée un graphique qui sait comment fournir une instance de LoginViewModel et, de manière récursive, de ses dépendances. Dagger sait comment procéder en raison de l'annotation @Inject sur le constructeur des classes.

Dans l'élément ApplicationComponent généré par Dagger, il existe une méthode de type fabrique pour obtenir les instances de toutes les classes qu'elle sait comment fournir. Dans cet exemple, Dagger délègue à l'élément NetworkModule inclus dans l'élément ApplicationComponent pour obtenir une instance de LoginRetrofitService.

Champs d'application Dagger

Les champs d'application sont décrits sur la page des Principes de base de Dagger comme un moyen d'avoir une seule instance d'un type donné dans un composant. C'est ce que l'on entend par définir le champ d'application d'un type sur le cycle de vie du composant.

Si vous souhaitez utiliser UserRepository dans d'autres fonctionnalités de l'application sans devoir créer un nouvel objet chaque fois que vous en avez besoin, vous pouvez le désigner comme instance unique pour l'ensemble de l'application. Il en va de même pour LoginRetrofitService : sa création peut s'avérer coûteuse de sorte que vous pourriez souhaiter réutiliser une instance unique de cet objet. La création d'une instance UserRemoteDataSource n'est pas aussi onéreuse. Il n'est donc pas nécessaire de la limiter au cycle de vie du composant.

@Singleton est la seule annotation de champ d'application fournie avec le package javax.inject. Vous pouvez l'utiliser pour annoter ApplicationComponent et les objets que vous souhaitez réutiliser dans l'ensemble de l'application.

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

Veillez à ne pas introduire de fuites de mémoire lorsque vous appliquez des champs d'application aux objets. Tant que le composant restreint est en mémoire, l'objet créé l'est également. Comme ApplicationComponent est créé lors du lancement de l'application (dans la classe Application), il est détruit avec l'application. Ainsi, l'instance unique de UserRepository reste toujours en mémoire jusqu'à la destruction de l'application.

Sous-composants de Dagger

Si votre flux de connexion (géré par un seul élément LoginActivity) consiste en plusieurs fragments, vous devez réutiliser la même instance de LoginViewModel dans tous les fragments. @Singleton ne peut pas annoter LoginViewModel pour réutiliser l'instance pour les raisons suivantes :

  1. L'instance de LoginViewModel resterait en mémoire une fois le flux terminé.

  2. Vous souhaitez une instance différente de LoginViewModel pour chaque flux de connexion. Par exemple, si l'utilisateur se déconnecte, vous souhaitez une instance différente de LoginViewModel plutôt que la même instance que lorsque l'utilisateur s'est connecté pour la première fois.

Pour limiter LoginViewModel au cycle de vie de LoginActivity, vous devez créer un nouveau composant (un nouveau sous-graphique) pour le flux de connexion et un nouveau champ d'application.

Créons maintenant un graphique spécifique au flux de connexion.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

LoginActivity devrait à présent recevoir des injections de LoginComponent en raison de sa configuration spécifique à la connexion. Cela supprime la responsabilité d'injecter LoginActivity dans la classe ApplicationComponent.

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent doit pouvoir accéder aux objets à partir de ApplicationComponent, car LoginViewModel dépend de UserRepository. Pour indiquer à Dagger que vous souhaitez qu'un nouveau composant utilise une partie d'un autre composant, vous pouvez utiliser les sous-composants Dagger. Le nouveau composant doit être un sous-composant de celui contenant des ressources partagées.

Les sous-composants sont les composants qui héritent du graphique d'objet d'un composant parent et le développent. Ainsi, tous les objets fournis dans le composant parent sont également fournis dans le sous-composant. De cette manière, un objet d'un sous-composant peut dépendre d'un objet fourni par le composant parent.

Pour créer des instances de sous-composants, vous avez besoin d'une instance du composant parent. Les objets fournis par le composant parent au sous-composant sont donc toujours limités au composant parent.

Dans l'exemple, vous devez définir LoginComponent comme sous-composant de ApplicationComponent. Pour ce faire, annotez LoginComponent avec @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);
}

Vous devez également définir une fabrique de sous-composants dans LoginComponent pour qu'ApplicationComponent sache comment créer des instances 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);
}

Pour faire savoir à Dagger que LoginComponent est un sous-composant de ApplicationComponent, vous devez l'indiquer comme suit :

  1. Créez un module Dagger (par exemple, SubcomponentsModule) en transmettant la classe du sous-composant à l'attribut subcomponents de l'annotation.

    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. Ajoutez le nouveau module (c'est-à-dire SubcomponentsModule) à 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 {
    }
    

    Notez que l'élément ApplicationComponent n'a plus besoin d'injecter LoginActivity, car c'est maintenant LoginComponent qui en est responsable. Vous pouvez donc supprimer la méthode inject() de l'élément ApplicationComponent.

    Les utilisateurs de l'élément ApplicationComponent doivent savoir comment créer des instances de LoginComponent. Le composant parent doit ajouter une méthode dans son interface pour permettre aux utilisateurs de créer des instances du sous-composant à partir d'une instance du composant parent :

  3. Exposez la fabrique qui crée des instances de LoginComponent dans l'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();
    }
    

Attribuer des champs d'application à des sous-composants

Si vous compilez le projet, vous pouvez créer des instances à la fois de l'élément ApplicationComponent et de LoginComponent. ApplicationComponent est associé au cycle de vie de l'application, car vous souhaitez utiliser la même instance du graphique tant que l'application est en mémoire.

Quel est le cycle de vie de LoginComponent ? L'une des raisons pour lesquelles vous avez besoin de LoginComponent est que vous devez partager la même instance de LoginViewModel entre les fragments associés à la connexion. Mais vous voulez également des instances différentes de LoginViewModel chaque fois qu'un nouveau flux de connexion est créé. LoginActivity est la durée de vie idéale pour LoginComponent : pour chaque nouvelle activité, vous avez besoin d'une nouvelle instance de LoginComponent et de fragments pouvant utiliser cette instance de LoginComponent.

Comme LoginComponent est associé au cycle de vie de LoginActivity, vous devez conserver une référence au composant de l'activité de la même manière que vous avez gardé la référence à applicationComponent dans la classe Application. Les fragments peuvent ainsi y accéder.

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;

    ...
}

Notez que la variable loginComponent n'est pas annotée avec @Inject, car vous ne prévoyez pas que cette variable sera fournie par Dagger.

Vous pouvez utiliser ApplicationComponent pour obtenir une référence à LoginComponent, puis injecter LoginActivity comme suit :

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 est créé dans la méthode onCreate() de l'activité et est implicitement détruit en même temps que l'activité.

LoginComponent doit toujours fournir la même instance de LoginViewModel à chaque requête. Pour ce faire, créez un champ d'application d'annotation personnalisé et annotez-le à la fois avec LoginComponent et LoginViewModel. Notez que vous ne pouvez pas utiliser l'annotation @Singleton, car elle a déjà été utilisée par le composant parent, ce qui en ferait un Singleton d'application (instance unique pour l'ensemble de l'application). Vous devez créer un champ d'application différent.

Dans ce cas, vous pouvez appeler ce champ d'application @LoginScope, mais ce n'est pas recommandé. Le nom de l'annotation de champ d'application ne doit pas mentionner explicitement l'objectif rempli. Il doit plutôt être défini en fonction de sa durée de vie, car les annotations peuvent être réutilisées par des composants frères comme RegistrationComponent et SettingsComponent. Vous devez donc appeler l'annotation @ActivityScope plutôt que @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;
    }
}

Si deux fragments nécessitent LoginViewModel, ils sont fournis avec la même instance. Par exemple, si vous avez un élément LoginUsernameFragment et un élément LoginPasswordFragment, ils doivent être injectés par 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);
}

Les composants accèdent à l'instance du composant qui se trouve dans l'objet LoginActivity. L'exemple de code pour LoginUserNameFragment apparaît dans l'extrait de code suivant :

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

Il en va de même pour 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)
    }
}

L'image 3 montre à quoi ressemble le graphique Dagger avec le nouveau sous-composant. Les classes avec un point blanc (UserRepository, LoginRetrofitService et LoginViewModel) sont celles qui disposent d'une instance unique limitée à leurs composants respectifs.

Graphique de l'application après l'ajout du dernier sous-composant

Figure 3. Représentation du graphique que vous avez créé pour l'exemple d'application Android

Analysons chaque partie du graphique :

  1. NetworkModule (et donc LoginRetrofitService) est inclus dans ApplicationComponent, car vous l'avez spécifié dans le composant.

  2. UserRepository reste dans ApplicationComponent, car il est limité à ApplicationComponent. Si le projet se développe, vous pouvez partager la même instance entre différentes fonctionnalités (par exemple, l'inscription).

    Comme UserRepository fait partie de l'élément ApplicationComponent, ses dépendances (par exemple, UserLocalDataSource et UserRemoteDataSource) doivent également se trouver dans ce composant afin de pouvoir fournir des instances de UserRepository.

  3. LoginViewModel est inclus dans LoginComponent, car il n'est requis que par les classes injectées par LoginComponent. LoginViewModel n'est pas inclus dans ApplicationComponent, car aucune dépendance dans ApplicationComponent n'a besoin de LoginViewModel.

    De même, si vous n'aviez pas limité UserRepository à ApplicationComponent, Dagger aurait inclus automatiquement UserRepository et ses dépendances dans LoginComponent, car c'est actuellement le seul endroit où UserRepository est utilisé.

En plus de définir un autre cycle de vie aux objets, la création de sous-composants est recommandée pour encapsuler différentes parties de votre application les unes aux autres.

Structurer votre application pour créer différents sous-graphiques Dagger en fonction du flux de votre application permet d'obtenir une application plus performante et évolutive en termes de mémoire et de temps de démarrage.

Bonnes pratiques de création d'un graphique Dagger

Lors de la création d'un graphique Dagger pour votre application :

  • Lorsque vous créez un composant, vous devez déterminer quel élément est responsable de sa durée de vie. Dans ce cas, la classe Application est responsable de l'élément ApplicationComponent, et LoginActivity est responsable de LoginComponent.

  • N'utilisez le champ d'application que lorsque c'est pertinent. L'utilisation excessive du champ d'application peut avoir un impact négatif sur les performances d'exécution de votre application : l'objet reste en mémoire tant que le composant l'est également et que l'obtention d'un objet avec un champ d'application est plus coûteux. Lorsque Dagger fournit l'objet, il utilise le verrouillage DoubleCheck au lieu d'un fournisseur de type fabrique.

Tester un projet qui utilise Dagger

L'un des avantages des frameworks d'injection de dépendances comme Dagger est qu'ils facilitent le test de votre code.

Tests unitaires

Inutile d'utiliser Dagger pour les tests unitaires. Lorsque vous testez une classe qui utilise l'injection par constructeur, vous n'avez pas besoin d'utiliser Dagger pour instancier cette classe. Vous pouvez directement appeler son constructeur en transmettant des dépendances factices ou fictives, exactement comme vous le feriez si elles n'étaient pas annotées.

Par exemple, lorsque vous testez 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(...);
    }
}

Tests de bout en bout

Pour les tests d'intégration, il est recommandé de créer un TestApplicationComponent conçu spécifiquement pour le test. La production et le test utilisent une configuration de composant différente.

Cette opération nécessite une conception des modules anticipée pour votre application. Le composant de test étend le composant de production et installe un autre ensemble de modules.

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 dispose d'une fausse intégration de NetworkModule. Vous pouvez alors fournir de fausses instances ou des simulations de tout ce que vous souhaitez remplacer.

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

Dans vos tests d'intégration ou de bout en bout, vous devez utiliser un élément TestApplication qui crée l'élément TestApplicationComponent au lieu d'un élément 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();
}

Cette application de test est ensuite utilisée dans un élément TestRunner personnalisé que vous utiliserez pour exécuter des tests d'instrumentation. Pour en savoir plus à ce sujet, consultez l'atelier de programmation de l'utilisation de Dagger dans votre application Android.

Utiliser les modules Dagger

Les modules Dagger permettent d'encapsuler la façon de fournir des objets de manière sémantique. Vous pouvez inclure des modules dans des composants, mais également dans d'autres modules. Cette méthode est efficace, mais elle peut facilement être utilisée à l'excès.

Lorsqu'un module a été ajouté à un composant ou à un autre module, il apparaît déjà dans le graphique Dagger. Dagger peut fournir ces objets dans ce composant. Avant d'ajouter un module, vérifiez s'il fait déjà partie du graphique Dagger. Pour cela, vérifiez s'il est déjà ajouté au composant ou compilez le projet, puis vérifiez si Dagger peut trouver les dépendances requises pour ce module.

Selon les bonnes pratiques, les modules ne doivent être déclarés qu'une seule fois dans un composant (en dehors de cas d'utilisation spécifiques avancés de Dagger).

Imaginons que vous ayez configuré votre graphique de cette manière. ApplicationComponent inclut Module1 et Module2, et Module1 inclut 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 { ... }

Si c'est le cas, Module2 dépend des classes fournies par ModuleX. Une mauvaise pratique inclut ModuleX dans Module2, car ModuleX est inclus deux fois dans le graphique, comme l'illustre l'extrait de code suivant :

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

Effectuez plutôt l'une des opérations suivantes :

  1. Refactorisez les modules et extrayez le module commun du composant.
  2. Créez un nouveau module avec les objets partagés par les deux modules et extrayez-le dans le composant.

Une méthode de refactorisation différente entraîne l'inclusion d'un grand nombre de modules les uns dans les autres sans structure claire. Il est alors plus compliqué de comprendre d'où vient chaque dépendance.

Bonne pratique (Option 1) : ModuleX est déclaré une fois dans le graphique 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 { ... }

Bonne pratique (option 2) : les dépendances courantes de Module1 et Module2 en ModuleX sont extraites vers un nouveau module nommé ModuleXCommon, inclus dans le composant. Ensuite, deux autres modules nommés ModuleXWithModule1Dependencies et ModuleXWithModule2Dependencies sont créés avec les dépendances propres à chaque module. Tous les modules sont déclarés une fois dans le graphique 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 { ... }

Injection assistée

L'injection assistée est un modèle DI utilisé pour construire un objet dans lequel certains paramètres peuvent être fournis par le framework DI, et d'autres transmis par l'utilisateur au moment de la création.

Dans Android, ce modèle est courant dans les écrans details où l'ID de l'élément à afficher n'est connu qu'au moment de l'exécution, et non au moment de la compilation, lorsque Dagger génère le graphique DI. Pour en savoir plus sur l'injection assistée avec Dagger, consultez les documents relatifs à Dagger.

Conclusion

Si vous ne l'avez pas déjà fait, consultez la section des bonnes pratiques. Pour savoir comment utiliser Dagger dans une application Android, consultez l'atelier de programmation de l'utilisation de Dagger dans votre application Android.