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.
- Utilisez
- 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.
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 :
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 :
L'instance de
LoginViewModel
resterait en mémoire une fois le flux terminé.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 deLoginViewModel
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 :
Créez un module Dagger (par exemple,
SubcomponentsModule
) en transmettant la classe du sous-composant à l'attributsubcomponents
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 { }
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'injecterLoginActivity
, car c'est maintenantLoginComponent
qui en est responsable. Vous pouvez donc supprimer la méthodeinject()
de l'élémentApplicationComponent
.Les utilisateurs de l'élément
ApplicationComponent
doivent savoir comment créer des instances deLoginComponent
. 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 :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.
Analysons chaque partie du graphique :
NetworkModule
(et doncLoginRetrofitService
) est inclus dansApplicationComponent
, car vous l'avez spécifié dans le composant.UserRepository
reste dansApplicationComponent
, 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émentApplicationComponent
, ses dépendances (par exemple,UserLocalDataSource
etUserRemoteDataSource
) doivent également se trouver dans ce composant afin de pouvoir fournir des instances deUserRepository
.LoginViewModel
est inclus dansLoginComponent
, car il n'est requis que par les classes injectées parLoginComponent
.LoginViewModel
n'est pas inclus dansApplicationComponent
, car aucune dépendance dansApplicationComponent
n'a besoin deLoginViewModel
.De même, si vous n'aviez pas limité
UserRepository
àApplicationComponent
, Dagger aurait inclus automatiquementUserRepository
et ses dépendances dansLoginComponent
, 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émentApplicationComponent
, etLoginActivity
est responsable deLoginComponent
.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 :
- Refactorisez les modules et extrayez le module commun du composant.
- 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.