L'injection de dépendances (DI) est un modèle de conception utilisé pour implémenter l'inversion de contrôle (IoC) où le contrôle de la création et de la gestion des dépendances est transféré de l'application à une entité externe. Cela permet de créer un code plus modulaire, testable et maintenable. Il s'agit d'une technique où la responsabilité de la création d'objets est transférée à d'autres parties du code. Cela favorise un couplage lâche, rendant le code plus modulaire et plus facile à gérer.
Les classes ont souvent besoin de références à d'autres classes pour fonctionner correctement. Par exemple, considérons une classe Library
qui nécessite une classe Book
. Ces classes nécessaires sont appelées dépendances. La classe Library
a besoin d'une instance de la classe Book
pour fonctionner.
Il existe trois manières principales pour une classe d'obtenir les objets dont elle a besoin :
Library
créerait et initialiserait sa propre instance de la classe Book
.Context
et getSystemService()
, fonctionnent de cette manière.Library
recevrait une instance Book
en tant que paramètre.La troisième option est l'injection de dépendances ! Avec l'injection de dépendances, vous fournissez les dépendances d'une classe plutôt que de laisser l'instance de classe les obtenir elle-même.
Sans DI, une Library
qui crée sa propre dépendance Book
pourrait ressembler à ceci :
class Library { private Book book = new Book(); void open() { book.read(); } } public class Main { public static void main(String[] args) { Library library = new Library(); library.open(); } }
Il ne s'agit pas d'un exemple de DI car la classe Library
construit son propre Book
. Cela peut être problématique car :
Library
et Book
sont étroitement couplés. Une instance de Library
utilise un type de Book
, ce qui rend difficile l'utilisation de sous-classes ou d'implémentations alternatives.Book
rend les tests plus difficiles. Library
utilise une instance réelle de Book
, ce qui empêche l'utilisation de doublons de test pour modifier Book
pour différents cas de test. Avec DI, au lieu que chaque instance de Library
construise son propre objet Book
, elle reçoit un objet Book
comme paramètre dans son constructeur :
class Library { private Book book; Library(Book book) { this.book = book; } void open() { book.read(); } } public class Main { public static void main(String[] args) { Book book = new Book(); Library library = new Library(book); library.open(); }
La fonction principale utilise Library
. Étant donné que Library
dépend de Book
, l'application crée une instance de Book
et l'utilise ensuite pour construire une instance de Library
. Les avantages de cette approche basée sur l'injection de dépendances sont les suivants :
Library
: vous pouvez transmettre différentes implémentations de Book
à Library
. Par exemple, vous pouvez définir une nouvelle sous-classe de Book
appelée EBook
que vous souhaitez que Library
utilise. Avec DI, vous transmettez simplement une instance de EBook
à Library
, et cela fonctionne sans aucune autre modification.Library
: vous pouvez passer des tests doublons pour tester différents scénarios. Considérez un scénario dans lequel une classe NotificationService
s'appuie sur une classe Notification
. Sans DI, NotificationService
crée directement une instance de Notification
, ce qui rend difficile l'utilisation de différents types de notifications ou le test du service avec diverses implémentations de notifications.
Pour illustrer DI, refactorisons cet exemple :
interface Notification { void send(); } class EmailNotification implements Notification { @Override public void send() { // Send email notification } } class SMSNotification implements Notification { @Override public void send() { // Send SMS notification } } class NotificationService { void sendNotification(Notification notification) { notification.send(); } }
Désormais, NotificationService
dépend de l'interface Notification
plutôt que d'une classe spécifique. Cela permet d'utiliser différentes implémentations de Notification
de manière interchangeable. Vous pouvez définir l'implémentation que vous souhaitez utiliser via la méthode sendNotification
:
NotificationService service = new NotificationService(); service.sendNotification(new EmailNotification()); service.sendNotification(new SMSNotification());
Il existe trois principaux types de DI :
class NotificationService { private final Notification notification; public NotificationService(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(new EmailNotification()); service.sendNotification(); } }
3. Injection de champ (ou injection de setter) : certaines classes du framework Android, telles que les activités et les fragments, sont instanciées par le système, de sorte que l'injection de constructeur n'est pas possible. Avec l'injection de champ, les dépendances sont instanciées après la création de la classe.
class NotificationService { private Notification notification; public Notification getNotification() { return notification; } public void setNotification(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(); service.setNotification(new EmailNotification()); service.sendNotification(); } }
4. Injection de méthode : les dépendances sont fournies via des méthodes, souvent à l'aide de l'annotation @Inject
.
Dans l'exemple précédent, vous avez créé, fourni et géré manuellement les dépendances de différentes classes sans utiliser de bibliothèque. Cette approche est connue sous le nom d'injection de dépendances manuelle. Bien qu'elle fonctionne pour les cas simples, elle devient fastidieuse à mesure que le nombre de dépendances et de classes augmente. L'injection de dépendances manuelle présente plusieurs inconvénients :
Les bibliothèques peuvent automatiser ce processus en créant et en fournissant des dépendances pour vous. Ces bibliothèques se répartissent en deux catégories :
Dagger est une bibliothèque d'injection de dépendances populaire pour Java, Kotlin et Android, gérée par Google. Dagger simplifie l'injection de dépendances dans votre application en créant et en gérant le graphique de dépendances pour vous. Il fournit des dépendances entièrement statiques au moment de la compilation, résolvant de nombreux problèmes de développement et de performances associés aux solutions basées sur la réflexion comme Guice.
Ces frameworks connectent les dépendances au moment de l'exécution :
Ces frameworks génèrent du code pour connecter les dépendances au moment de la compilation :
Une alternative à l'injection de dépendances est le modèle de localisateur de services. Ce modèle de conception permet également de découpler les classes de leurs dépendances concrètes. Vous créez une classe appelée localisateur de services qui crée et stocke les dépendances, en les fournissant à la demande.
object ServiceLocator { fun getProcessor(): Processor = Processor() } class Computer { private val processor = ServiceLocator.getProcessor() fun start() { processor.run() } } fun main(args: Array<String>) { val computer = Computer() computer.start() }
Le modèle de localisateur de services diffère de l'injection de dépendances dans la manière dont les dépendances sont consommées. Avec le modèle de localisateur de services, les classes demandent les dépendances dont elles ont besoin ; avec l'injection de dépendances, l'application fournit de manière proactive les objets requis.
Dagger 2 est un framework DI populaire pour Android. Il utilise la génération de code au moment de la compilation et est connu pour ses hautes performances. Dagger 2 simplifie le processus d'injection de dépendances en générant le code nécessaire pour gérer les dépendances, en réduisant le code standard et en améliorant l'efficacité.
Dagger 2 est une bibliothèque basée sur des annotations pour l'injection de dépendances dans Android. Voici les principales annotations et leurs objectifs :
ApiClient
pour Retrofit.@Module
et @Inject
. Elle contient tous les modules et fournit le générateur pour l'application.@Provides
mais plus concis. Dagger peut générer un graphique de dépendances pour votre projet, lui permettant de déterminer où obtenir les dépendances en cas de besoin. Pour cela, vous devez créer une interface et l'annoter avec @Component
.
Dans l'interface @Component
, vous définissez des méthodes qui renvoient des instances des classes dont vous avez besoin (par exemple, BookRepository
). L'annotation @Component
demande à Dagger de générer un conteneur avec toutes les dépendances requises pour satisfaire les types qu'il expose. Ce conteneur est connu sous le nom de composant Dagger et il contient un graphique d'objets que Dagger sait fournir avec leurs dépendances.
Considérons un exemple impliquant un LibraryRepository
:
@Inject
au constructeur LibraryRepository
afin que Dagger sache comment créer une instance de LibraryRepository
. public class LibraryRepository { private final LocalLibraryDataSource localDataSource; private final RemoteLibraryDataSource remoteDataSource; @Inject public LibraryRepository(LocalLibraryDataSource localDataSource, RemoteLibraryDataSource remoteDataSource) { this.localDataSource = localDataSource; this.remoteDataSource = remoteDataSource; } }
2. Annoter les dépendances : De même, annotez les constructeurs des dépendances ( LocalLibraryDataSource
et RemoteLibraryDataSource
) afin que Dagger sache comment les créer.
public class LocalLibraryDataSource { @Inject public LocalLibraryDataSource() { // Initialization code } } public class RemoteLibraryDataSource { private final LibraryService libraryService; @Inject public RemoteLibraryDataSource(LibraryService libraryService) { this.libraryService = libraryService; } }
3. Définir le composant : Créez une interface annotée avec @Component
pour définir le graphe de dépendances.
@Component public interface ApplicationComponent { LibraryRepository getLibraryRepository(); }
Lorsque vous générez le projet, Dagger génère pour vous une implémentation de l'interface ApplicationComponent
, généralement nommée DaggerApplicationComponent
.
Vous pouvez maintenant utiliser le composant généré pour obtenir des instances de vos classes avec leurs dépendances injectées automatiquement :
public class MainApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.create(); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
Dans votre activité ou fragment, vous pouvez récupérer l'instance LibraryRepository
:
public class MainActivity extends AppCompatActivity { @Inject LibraryRepository libraryRepository; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MainApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected libraryRepository } }
1. Modules
∘ Concepts clés des modules
∘ Inclure des modules dans les composants
2. Portées
3. Composants
4. Dépendances des composants
5. Liaisons d'exécution
Les modules de Dagger 2 sont des classes annotées avec @Module
qui fournissent des dépendances aux composants. Ils contiennent des méthodes annotées avec @Provides
ou @Binds
pour spécifier comment créer et fournir des dépendances. Les modules sont essentiels pour organiser et gérer la création des objets dont votre application a besoin.
@Provides
et est utilisée lorsque le module est une classe abstraite.Exemple de module
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } @Provides @Singleton OkHttpClient provideOkHttpClient() { return new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build(); } }
Dans cet exemple, NetworkModule
est une classe annotée avec @Module
. Elle contient deux méthodes annotées avec @Provides
qui créent et renvoient des instances de Retrofit
et OkHttpClient
.
Utilisation de @Binds
Lorsque vous disposez d'une interface et de son implémentation, vous pouvez utiliser @Binds
pour lier l'implémentation à l'interface. C'est plus concis que d'utiliser @Provides
.
public interface ApiService { void fetchData(); } public class ApiServiceImpl implements ApiService { @Override public void fetchData() { // Implementation } } @Module public abstract class ApiModule { @Binds abstract ApiService bindApiService(ApiServiceImpl apiServiceImpl); }
Dans cet exemple, ApiModule
est une classe abstraite annotée avec @Module
. La méthode bindApiService
est annotée avec @Binds
pour lier ApiServiceImpl
à ApiService
.
Les modules peuvent être organisés en fonction des fonctionnalités qu'ils fournissent. Par exemple, vous pouvez disposer de modules distincts pour les opérations réseau, les opérations de base de données et les dépendances liées à l'interface utilisateur.
Exemple:
Retrofit
et OkHttpClient
.RoomDatabase
.ViewModel
et Presenter
.Les modules sont inclus dans les composants pour fournir des dépendances aux classes qui en ont besoin. Voici comment vous pouvez le configurer :
ApplicationComponent.java :
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Dans cet exemple, ApplicationComponent
inclut NetworkModule
et DatabaseModule
pour fournir des dépendances à l'application.
Les portées dans Dagger 2 sont des annotations qui définissent le cycle de vie des dépendances. Elles garantissent qu'une seule instance d'une dépendance est créée et partagée dans une portée spécifiée. Cela permet de gérer efficacement la mémoire et de garantir que les dépendances sont réutilisées le cas échéant.
1. Portée Singleton
Définition : La portée @Singleton
garantit qu'une seule instance d'une dépendance est créée et partagée tout au long du cycle de vie de l'application.
Cette portée est généralement utilisée pour les dépendances qui doivent être partagées dans l'ensemble de l'application, telles que les clients réseau, les instances de base de données ou les préférences partagées.
Exemple:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Dans cet exemple, l'annotation @Singleton
garantit que les instances Retrofit
et Database
fournies par NetworkModule
et DatabaseModule
sont des singletons et partagées dans l'ensemble de l'application.
2. Champ d'activité
Définition : @ActivityScope
(une portée personnalisée) garantit qu'une seule instance d'une dépendance est créée et partagée au cours du cycle de vie d'une activité.
Cette portée est utile pour les dépendances spécifiques à une activité et doivent être recréées à chaque recréation de l'activité, comme les présentateurs ou les modèles de vue.
Exemple :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope { } @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
Dans cet exemple, l'annotation @ActivityScope
garantit que les dépendances fournies par ActivityModule
sont limitées au cycle de vie de l'activité.
3. Portée du fragment
Définition : @FragmentScope
(une autre portée personnalisée) garantit qu'une seule instance d'une dépendance est créée et partagée dans le cycle de vie d'un fragment.
Cas d'utilisation : cette portée est utile pour les dépendances spécifiques à un fragment et doivent être recréées à chaque recréation du fragment, comme les présentateurs ou les modèles de vue spécifiques à un fragment.
Exemple :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScope { } @FragmentScope @Component(dependencies = ActivityComponent.class, modules = FragmentModule.class) public interface FragmentComponent { void inject(MyFragment myFragment); }
Dans cet exemple, l'annotation @FragmentScope
garantit que les dépendances fournies par FragmentModule
sont limitées au cycle de vie du fragment.
Les dépendances entre composants permettent à un composant de dépendre d'un autre, ce qui permet la réutilisation des dépendances. Il existe deux principaux types de dépendances entre composants :
1. Composant d'application
Définition : Le composant d'application fournit des dépendances nécessaires à l'ensemble de l'application. Il est généralement limité à @Singleton
pour garantir que les dépendances sont partagées dans toute l'application.
Ce composant est utilisé pour les dépendances qui doivent être disponibles à l'échelle mondiale, telles que les clients réseau, les instances de base de données ou les préférences partagées.
Exemple :
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Dans cet exemple, ApplicationComponent
est responsable de la fourniture des instances Retrofit
et Database
, qui sont partagées dans l'ensemble de l'application.
2. Composante d'activité
Définition : Le composant Activity fournit les dépendances nécessaires à une activité spécifique. Il est généralement défini avec une portée personnalisée, telle que @ActivityScope
, pour garantir que les dépendances sont recréées à chaque fois que l'activité est recréée.
Ce composant est utilisé pour les dépendances spécifiques à une activité, telles que les présentateurs ou les modèles de vue.
Exemple :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
Dans cet exemple, ActivityComponent
dépend d' ApplicationComponent
et fournit des dépendances spécifiques à MainActivity
.
Les dépendances entre composants permettent à un composant de dépendre d'un autre, ce qui permet la réutilisation des dépendances. Il existe deux principaux types de dépendances entre composants :
1. Sous-composants :
Un sous-composant est un enfant d'un autre composant et peut accéder aux dépendances de son parent. Les sous-composants sont définis dans le composant parent et peuvent hériter de sa portée.
Exemple :
@ActivityScope @Subcomponent(modules = ActivityModule.class) public interface ActivitySubcomponent { void inject(MainActivity mainActivity); }
Dans cet exemple, ActivitySubcomponent
est un sous-composant du composant parent et peut accéder à ses dépendances.
2. Attribut de dépendance
Cela permet à un composant de dépendre d'un autre composant sans être un sous-composant. Le composant dépendant peut accéder aux dépendances fournies par le composant parent.
Exemple :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
Dans cet exemple, ActivityComponent
dépend d' ApplicationComponent
et peut accéder à ses dépendances.
Les liaisons d'exécution dans Dagger 2 font référence à la fourniture de dépendances créées et gérées au moment de l'exécution, en fonction du contexte dans lequel elles sont nécessaires.
1. Contexte d'application
Définition : Le contexte applicatif est un contexte lié au cycle de vie de l'application dans son ensemble. Il est utilisé pour les dépendances qui doivent vivre aussi longtemps que l'application elle-même.
Dépendances partagées dans l'ensemble de l'application et qui n'ont pas besoin d'être recréées pour chaque activité ou fragment. Les exemples incluent les clients réseau, les instances de base de données et les préférences partagées.
Exemple :
@Module public class AppModule { private final Application application; public AppModule(Application application) { this.application = application; } @Provides @Singleton Application provideApplication() { return application; } @Provides @Singleton Context provideApplicationContext() { return application.getApplicationContext(); } }
Dans cet exemple, AppModule
fournit le contexte de l'application sous forme de dépendance singleton. La méthode provideApplicationContext
garantit que le contexte fourni est lié au cycle de vie de l'application.
2. Contexte de l'activité
Définition : Le contexte d'activité est un contexte lié au cycle de vie d'une activité spécifique. Il est utilisé pour les dépendances qui doivent vivre aussi longtemps que l'activité elle-même.
Dépendances spécifiques à une activité et devant être recréées à chaque fois que l'activité est recréée. Exemples : modèles de vue, présentateurs et dépendances liées à l'interface utilisateur.
Exemple :
@Module public class ActivityModule { private final Activity activity; public ActivityModule(Activity activity) { this.activity = activity; } @Provides @ActivityScope Activity provideActivity() { return activity; } @Provides @ActivityScope Context provideActivityContext() { return activity; } }
Dans cet exemple, ActivityModule
fournit le contexte d'activité sous forme de dépendance limitée. La méthode provideActivityContext
garantit que le contexte fourni est lié au cycle de vie de l'activité.
Pour utiliser ces liaisons d'exécution, vous devez inclure les modules correspondants dans vos composants :
Composant d'application :
@Singleton @Component(modules = {AppModule.class, NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); Context getApplicationContext(); }
Composante de l'activité :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); Context getActivityContext(); }
Injection de contextes
Une fois que vous avez configuré vos composants et modules, vous pouvez injecter les contextes dans vos classes selon vos besoins.
Exemple dans une activité :
public class MainActivity extends AppCompatActivity { @Inject Context activityContext; @Inject Context applicationContext; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ApplicationComponent appComponent = ((MyApplication) getApplication()).getApplicationComponent(); ActivityComponent activityComponent = DaggerActivityComponent.builder() .applicationComponent(appComponent) .activityModule(new ActivityModule(this)) .build(); activityComponent.inject(this); // Use the injected contexts Log.d("MainActivity", "Activity Context: " + activityContext); Log.d("MainActivity", "Application Context: " + applicationContext); } }
Dans cet exemple, MainActivity
reçoit à la fois le contexte d'activité et le contexte d'application via l'injection de dépendances. Cela permet à l'activité d'utiliser le contexte approprié en fonction des besoins spécifiques des dépendances.
Pour utiliser Dagger 2 dans votre projet, vous devez ajouter les dépendances suivantes à votre fichier build.gradle
:
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Remplacez 2.x
par la dernière version de Dagger 2.
Créez un module pour fournir des dépendances. Par exemple, un NetworkModule
pour fournir une instance Retrofit
:
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } }
Créez un composant pour relier le module et les classes qui ont besoin des dépendances :
@Singleton @Component(modules = {NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Utilisez le composant pour injecter des dépendances dans vos classes. Par exemple, dans votre classe Application
:
public class MyApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.builder() .networkModule(new NetworkModule()) .build(); applicationComponent.inject(this); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
Vous pouvez maintenant utiliser les dépendances injectées dans vos classes. Par exemple, dans une Activity
:
public class MainActivity extends AppCompatActivity { @Inject Retrofit retrofit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MyApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected Retrofit instance // ... } }
Résumons ce sujet :