Einführung in die Abhängigkeitsinjektion
Dependency Injection (DI) ist ein Entwurfsmuster, das zur Implementierung von Inversion of Control (IoC) verwendet wird, wobei die Kontrolle über die Erstellung und Verwaltung von Abhängigkeiten von der Anwendung auf eine externe Entität übertragen wird. Dies hilft bei der Erstellung modulareren, testbareren und wartbareren Codes. Es handelt sich um eine Technik, bei der die Verantwortung für die Erstellung von Objekten auf andere Teile des Codes übertragen wird. Dies fördert eine lose Kopplung und macht den Code modularer und einfacher zu verwalten.
Klassen benötigen häufig Verweise auf andere Klassen, um richtig zu funktionieren. Betrachten Sie beispielsweise eine Library
, die eine Book
erfordert. Diese erforderlichen Klassen werden als Abhängigkeiten bezeichnet. Die Library
ist für ihre Funktion auf eine Instanz der Book
angewiesen.
Es gibt drei grundlegende Möglichkeiten für eine Klasse, die benötigten Objekte abzurufen:
- Selbstkonstruktion : Die Klasse erstellt und initialisiert ihre eigenen Abhängigkeiten. Beispielsweise würde die Klasse
Library
ihre eigene Instanz der KlasseBook
erstellen und initialisieren. - Externer Abruf : Die Klasse ruft Abhängigkeiten von einer externen Quelle ab. Einige Android-APIs, wie z. B.
Context
Getter undgetSystemService()
, funktionieren auf diese Weise. - Abhängigkeitsinjektion : Abhängigkeiten werden der Klasse entweder bei ihrer Erstellung oder durch Methoden, die sie erfordern, bereitgestellt. Beispielsweise würde der
Library
eineBook
als Parameter erhalten.
Die dritte Option ist die Abhängigkeitsinjektion! Mit DI stellen Sie die Abhängigkeiten einer Klasse bereit, anstatt sie von der Klasseninstanz selbst abrufen zu lassen.
Beispiel ohne Abhängigkeitsinjektion
Ohne DI könnte eine Library
, die ihre eigene Book
erstellt, folgendermaßen aussehen:
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(); } }
Dies ist kein Beispiel für DI, da die Klasse Library
ihr eigenes Book
erstellt. Dies kann aus folgenden Gründen problematisch sein:
- Enge Kopplung :
Library
undBook
sind eng gekoppelt. Eine Instanz vonLibrary
verwendet einenBook
, was die Verwendung von Unterklassen oder alternativen Implementierungen erschwert. - Testschwierigkeiten : Die starke Abhängigkeit von
Book
macht das Testen anspruchsvoller.Library
verwendet eine echte Instanz vonBook
und verhindert so die Verwendung von Testdoubles, umBook
für verschiedene Testfälle zu modifizieren.
Beispiel mit Abhängigkeitsinjektion
Mit DI erstellt nicht jede Instanz von Library
ihr eigenes Book
Objekt, sondern erhält ein Book
Objekt als Parameter in ihrem Konstruktor:
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(); }
Die Hauptfunktion verwendet Library
. Da Library
von Book
abhängt, erstellt die App eine Instanz von Book
und verwendet diese dann, um eine Instanz von Library
zu konstruieren. Die Vorteile dieses DI-basierten Ansatzes sind:
- Wiederverwendbarkeit von
Library
: Sie können verschiedene Implementierungen vonBook
anLibrary
übergeben. Sie können beispielsweise eine neue Unterklasse vonBook
namensEBook
definieren, dieLibrary
verwenden soll. Mit DI übergeben Sie einfach eine Instanz vonEBook
anLibrary
und es funktioniert ohne weitere Änderungen. - Einfaches Testen der
Library
: Sie können Test-Doubles übergeben, um verschiedene Szenarien zu testen.
Ein weiteres DI-Beispiel
Stellen Sie sich ein Szenario vor, in dem eine NotificationService
Klasse auf einer Notification
Klasse basiert. Ohne DI erstellt der NotificationService
direkt eine Instanz von Notification
, was die Verwendung unterschiedlicher Benachrichtigungstypen oder das Testen des Dienstes mit verschiedenen Benachrichtigungsimplementierungen erschwert.
Um DI zu veranschaulichen, überarbeiten wir dieses Beispiel:
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(); } }
Jetzt hängt NotificationService
von der Notification
Schnittstelle ab und nicht von einer bestimmten Klasse. Dadurch können verschiedene Implementierungen von Notification
austauschbar verwendet werden. Sie können die Implementierung, die Sie verwenden möchten, über die Methode sendNotification
festlegen:
NotificationService service = new NotificationService(); service.sendNotification(new EmailNotification()); service.sendNotification(new SMSNotification());
Methoden der Abhängigkeitsinjektion in Android
Es gibt drei Haupttypen von DI:
- Methodeninjektion (Schnittstelleninjektion) : Abhängigkeiten werden über Methoden übergeben, auf die die Klasse über eine Schnittstelle oder eine andere Klasse zugreifen kann. Das vorherige Beispiel demonstriert die Methodeninjektion.
- Konstruktorinjektion : Abhängigkeiten werden der Klasse über ihren Konstruktor übergeben.
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. Feldinjektion (oder Setter-Injektion) : Bestimmte Android-Framework-Klassen, wie Aktivitäten und Fragmente, werden vom System instanziiert, sodass eine Konstruktorinjektion nicht möglich ist. Bei der Feldinjektion werden Abhängigkeiten nach der Erstellung der Klasse instanziiert.
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. Methodeninjektion : Abhängigkeiten werden durch Methoden bereitgestellt, häufig unter Verwendung der Annotation @Inject
.
Vorteile der Abhängigkeitsinjektion
- Klassen werden wiederverwendbarer und sind weniger abhängig von bestimmten Implementierungen. Dies ist auf die Umkehrung der Steuerung zurückzuführen, bei der Klassen ihre Abhängigkeiten nicht mehr verwalten, sondern mit jeder bereitgestellten Konfiguration arbeiten.
- Abhängigkeiten sind Teil der API-Oberfläche und können bei der Objekterstellung oder Kompilierungszeit überprüft werden, was das Refactoring erleichtert.
- Da eine Klasse ihre Abhängigkeiten nicht verwaltet, können beim Testen unterschiedliche Implementierungen übergeben werden, um verschiedene Szenarien abzudecken.
Automatisierte Abhängigkeitsinjektion
Im vorherigen Beispiel haben Sie die Abhängigkeiten verschiedener Klassen manuell erstellt, bereitgestellt und verwaltet, ohne eine Bibliothek zu verwenden. Dieser Ansatz wird als manuelle Abhängigkeitsinjektion bezeichnet. Während dieser Ansatz in einfachen Fällen funktioniert, wird er umständlich, wenn die Anzahl der Abhängigkeiten und Klassen zunimmt. Die manuelle Abhängigkeitsinjektion hat mehrere Nachteile:
- Boilerplate-Code : Bei großen Anwendungen kann die Verwaltung aller Abhängigkeiten und deren korrekte Verbindung zu viel sich wiederholendem Code führen. In einer mehrschichtigen Architektur erfordert das Erstellen eines Objekts für eine oberste Schicht die Bereitstellung aller Abhängigkeiten für die darunter liegenden Schichten. Um beispielsweise einen Computer zu bauen, benötigen Sie eine CPU, ein Motherboard, RAM und andere Komponenten; und eine CPU benötigt möglicherweise Transistoren und Kondensatoren.
- Komplexe Abhängigkeitsverwaltung : Wenn Sie Abhängigkeiten nicht im Voraus erstellen können – etwa bei verzögerten Initialisierungen oder der Beschränkung von Objekten auf bestimmte Flows in Ihrer App – müssen Sie einen benutzerdefinierten Container (oder ein Abhängigkeitsdiagramm) schreiben und verwalten, um die Lebensdauer Ihrer Abhängigkeiten im Speicher zu verwalten.
Bibliotheken können diesen Prozess automatisieren, indem sie Abhängigkeiten für Sie erstellen und bereitstellen. Diese Bibliotheken fallen in zwei Kategorien:
- Reflexionsbasierte Lösungen : Diese verbinden Abhängigkeiten zur Laufzeit.
- Statische Lösungen : Diese generieren Code, um Abhängigkeiten zur Kompilierungszeit zu verbinden.
Dagger ist eine beliebte, von Google verwaltete Bibliothek zur Abhängigkeitsinjektion für Java, Kotlin und Android. Dagger vereinfacht die Abhängigkeitsinjektion in Ihrer App, indem es den Abhängigkeitsgraphen für Sie erstellt und verwaltet. Es bietet vollständig statische Abhängigkeiten zur Kompilierungszeit und behebt viele der Entwicklungs- und Leistungsprobleme, die mit reflexionsbasierten Lösungen wie Guice verbunden sind.
Reflexionsbasierte Lösungen
Diese Frameworks verbinden Abhängigkeiten zur Laufzeit:
- Toothpick : Ein Laufzeit-DI-Framework, das Reflektion zum Verbinden von Abhängigkeiten verwendet. Es ist leicht und schnell konzipiert und daher für Android-Anwendungen geeignet.
Statische Lösungen
Diese Frameworks generieren Code, um Abhängigkeiten zur Kompilierzeit zu verbinden:
- Hilt : Hilt basiert auf Dagger und bietet eine Standardmethode, um die Abhängigkeitsinjektion von Dagger in eine Android-Anwendung zu integrieren. Es vereinfacht die Einrichtung und Verwendung von Dagger, indem es vordefinierte Komponenten und Bereiche bereitstellt .
- Koin : Ein leichtes und einfaches DI-Framework für Kotlin. Koin verwendet eine DSL zur Definition von Abhängigkeiten und ist einfach einzurichten und zu verwenden.
- Kodein : Ein Kotlin-basiertes DI-Framework, das einfach zu verwenden und zu verstehen ist. Es bietet eine einfache und flexible API zur Verwaltung von Abhängigkeiten.
Alternativen zur Abhängigkeitsinjektion
Eine Alternative zur Abhängigkeitsinjektion ist das Service Locator-Muster. Dieses Entwurfsmuster hilft auch dabei, Klassen von ihren konkreten Abhängigkeiten zu entkoppeln. Sie erstellen eine Klasse namens Service Locator, die Abhängigkeiten erstellt und speichert und sie bei Bedarf bereitstellt.
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() }
Das Service Locator-Muster unterscheidet sich von der Abhängigkeitsinjektion in der Art und Weise, wie Abhängigkeiten genutzt werden. Beim Service Locator-Muster fordern Klassen die Abhängigkeiten an, die sie benötigen. Bei der Abhängigkeitsinjektion stellt die App die erforderlichen Objekte proaktiv bereit.
Was ist Dagger 2?
Dagger 2 ist ein beliebtes DI-Framework für Android. Es verwendet Codegenerierung zur Kompilierungszeit und ist für seine hohe Leistung bekannt. Dagger 2 vereinfacht den Prozess der Abhängigkeitsinjektion, indem es den erforderlichen Code zur Handhabung von Abhängigkeiten generiert, Boilerplate-Code reduziert und die Effizienz verbessert.
Dagger 2 ist eine annotationsbasierte Bibliothek für die Abhängigkeitsinjektion in Android. Hier sind die wichtigsten Annotationen und ihre Zwecke:
- @Module : Wird verwendet, um Klassen zu definieren, die Abhängigkeiten bereitstellen. Beispielsweise kann ein Modul einen
ApiClient
für Retrofit bereitstellen. - @Provides : Kommentiert Methoden in einem Modul, um anzugeben, wie Abhängigkeiten erstellt und zurückgegeben werden.
- @Inject : Wird zum Anfordern von Abhängigkeiten verwendet. Kann auf Felder, Konstruktoren und Methoden angewendet werden.
- @Component : Eine Schnittstelle, die
@Module
und@Inject
verbindet. Sie enthält alle Module und stellt den Builder für die Anwendung bereit. - @Singleton : Stellt sicher, dass eine einzelne Instanz einer Abhängigkeit erstellt wird.
- @Binds : Wird in abstrakten Klassen verwendet, um Abhängigkeiten bereitzustellen, ähnlich wie
@Provides
, aber prägnanter.
Dolchkomponenten
Dagger kann einen Abhängigkeitsgraphen für Ihr Projekt generieren, sodass es bei Bedarf feststellen kann, wo Abhängigkeiten abgerufen werden können. Um dies zu ermöglichen, müssen Sie eine Schnittstelle erstellen und diese mit @Component
kommentieren.
Innerhalb der @Component
-Schnittstelle definieren Sie Methoden, die Instanzen der benötigten Klassen zurückgeben (z. B. BookRepository
). Die @Component
Annotation weist Dagger an, einen Container mit allen Abhängigkeiten zu generieren, die erforderlich sind, um die von ihm bereitgestellten Typen zu erfüllen. Dieser Container wird als Dagger-Komponente bezeichnet und enthält ein Diagramm von Objekten, die Dagger zusammen mit ihren Abhängigkeiten bereitstellen kann.
Beispiel
Betrachten wir ein Beispiel mit einem LibraryRepository
:
- Kommentieren Sie den Konstruktor : Fügen Sie dem
LibraryRepository
Konstruktor eine@Inject
-Annotation hinzu, damit Dagger weiß, wie eine Instanz vonLibraryRepository
erstellt wird.
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. Abhängigkeiten kommentieren : Kommentieren Sie auf ähnliche Weise die Konstruktoren der Abhängigkeiten ( LocalLibraryDataSource
und RemoteLibraryDataSource
), damit Dagger weiß, wie sie erstellt werden.
public class LocalLibraryDataSource { @Inject public LocalLibraryDataSource() { // Initialization code } } public class RemoteLibraryDataSource { private final LibraryService libraryService; @Inject public RemoteLibraryDataSource(LibraryService libraryService) { this.libraryService = libraryService; } }
3. Definieren Sie die Komponente : Erstellen Sie eine mit @Component
kommentierte Schnittstelle, um den Abhängigkeitsgraphen zu definieren.
@Component public interface ApplicationComponent { LibraryRepository getLibraryRepository(); }
Wenn Sie das Projekt erstellen, generiert Dagger für Sie eine Implementierung der ApplicationComponent
Schnittstelle, die normalerweise DaggerApplicationComponent
heißt.
Verwendung
Sie können jetzt die generierte Komponente verwenden, um Instanzen Ihrer Klassen mit automatisch eingefügten Abhängigkeiten abzurufen:
public class MainApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.create(); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
In Ihrer Aktivität oder Ihrem Fragment können Sie die LibraryRepository
Instanz abrufen:
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 } }
Schlüsselkonzepte in Dagger 2
1. Module
∘ Schlüsselkonzepte der Module
∘ Module in Komponenten einbinden
2. Geltungsbereich
3. Komponenten
4. Komponentenabhängigkeiten
5. Laufzeitbindungen
1. Module
Module in Dagger 2 sind mit @Module
annotierte Klassen, die Abhängigkeiten zu den Komponenten bereitstellen. Sie enthalten mit @Provides
oder @Binds
annotierte Methoden, um anzugeben, wie Abhängigkeiten erstellt und bereitgestellt werden. Module sind für die Organisation und Verwaltung der Erstellung von Objekten, die Ihre Anwendung benötigt, unerlässlich.
Schlüsselkonzepte der Module
- @Module-Annotation: Diese Annotation wird verwendet, um eine Klasse als Dagger-Modul zu definieren. Eine Modulklasse enthält Methoden, die Abhängigkeiten bereitstellen.
- @Provides-Annotation: Diese Annotation wird bei Methoden innerhalb eines Moduls verwendet, um anzugeben, dass die Methode eine bestimmte Abhängigkeit bereitstellt. Diese Methoden sind für das Erstellen und Zurückgeben von Instanzen der Abhängigkeiten verantwortlich.
- @Binds-Annotation: Diese Annotation wird in abstrakten Klassen verwendet, um eine Implementierung an eine Schnittstelle zu binden. Sie ist prägnanter als
@Provides
und wird verwendet, wenn das Modul eine abstrakte Klasse ist.
Beispiel eines Moduls
@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(); } }
In diesem Beispiel ist NetworkModule
eine mit @Module
annotierte Klasse. Sie enthält zwei mit @Provides
annotierte Methoden, die Instanzen von Retrofit
und OkHttpClient
erstellen und zurückgeben.
Verwenden von @Binds
Wenn Sie eine Schnittstelle und deren Implementierung haben, können Sie @Binds
verwenden, um die Implementierung an die Schnittstelle zu binden. Dies ist prägnanter als die Verwendung von @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); }
In diesem Beispiel ist ApiModule
eine abstrakte Klasse, die mit @Module
annotiert ist. Die Methode bindApiService
ist mit @Binds
annotiert, um ApiServiceImpl
an ApiService
zu binden.
Module können basierend auf der von ihnen bereitgestellten Funktionalität organisiert werden. Sie können beispielsweise separate Module für Netzwerkvorgänge, Datenbankvorgänge und UI-bezogene Abhängigkeiten haben.
Beispiel:
- NetworkModule : Bietet netzwerkbezogene Abhängigkeiten wie
Retrofit
undOkHttpClient
. - DatabaseModule : Bietet datenbankbezogene Abhängigkeiten wie
RoomDatabase
. - UIModule : Bietet UI-bezogene Abhängigkeiten wie
ViewModel
undPresenter
.
Einbinden von Modulen in Komponenten
Module werden in Komponenten eingebunden, um Abhängigkeiten für die Klassen bereitzustellen, die sie benötigen. So können Sie dies einrichten:
Anwendungskomponente.java:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
In diesem Beispiel umfasst ApplicationComponent
NetworkModule
und DatabaseModule
, um Abhängigkeiten für die Anwendung bereitzustellen.
2. Geltungsbereich
Bereiche in Dagger 2 sind Anmerkungen, die den Lebenszyklus von Abhängigkeiten definieren. Sie stellen sicher, dass eine einzelne Instanz einer Abhängigkeit erstellt und innerhalb eines angegebenen Bereichs freigegeben wird. Dies hilft bei der effizienten Verwaltung des Speichers und stellt sicher, dass Abhängigkeiten bei Bedarf wiederverwendet werden.
- Singleton-Bereich : Stellt sicher, dass während des gesamten Lebenszyklus der Anwendung eine einzelne Instanz einer Abhängigkeit vorhanden ist.
- Aktivitätsumfang : Stellt eine einzelne Instanz einer Abhängigkeit innerhalb des Lebenszyklus einer Aktivität sicher.
- Fragmentumfang : Stellt eine einzelne Instanz einer Abhängigkeit innerhalb des Lebenszyklus eines Fragments sicher.
1. Singleton-Bereich
Definition : Der @Singleton
-Bereich stellt sicher, dass während des gesamten Lebenszyklus der Anwendung eine einzelne Instanz einer Abhängigkeit erstellt und gemeinsam genutzt wird.
Dieser Bereich wird normalerweise für Abhängigkeiten verwendet, die von der gesamten Anwendung gemeinsam genutzt werden müssen, z. B. Netzwerkclients, Datenbankinstanzen oder gemeinsame Einstellungen.
Beispiel:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
In diesem Beispiel stellt die Annotation @Singleton
sicher, dass die von NetworkModule
und DatabaseModule
bereitgestellten Retrofit
und Database
Singletons sind und in der gesamten Anwendung gemeinsam genutzt werden.
2. Tätigkeitsbereich
Definition : @ActivityScope
(ein benutzerdefinierter Bereich) stellt sicher, dass innerhalb des Lebenszyklus einer Aktivität eine einzelne Instanz einer Abhängigkeit erstellt und freigegeben wird.
Dieser Bereich ist nützlich für Abhängigkeiten, die spezifisch für eine Aktivität sind und bei jeder Neuerstellung der Aktivität neu erstellt werden sollten, wie z. B. Präsentatoren oder Ansichtsmodelle.
Beispiel :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope { } @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
In diesem Beispiel stellt die Annotation @ActivityScope
sicher, dass die von ActivityModule
bereitgestellten Abhängigkeiten auf den Lebenszyklus der Aktivität beschränkt sind.
3. Fragmentumfang
Definition : @FragmentScope
(ein weiterer benutzerdefinierter Bereich) stellt sicher, dass innerhalb des Lebenszyklus eines Fragments eine einzelne Instanz einer Abhängigkeit erstellt und gemeinsam genutzt wird.
Anwendungsfall: Dieser Bereich ist nützlich für Abhängigkeiten, die spezifisch für ein Fragment sind und bei jeder Neuerstellung des Fragments neu erstellt werden sollten, z. B. fragmentspezifische Presenter oder Ansichtsmodelle.
Beispiel :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScope { } @FragmentScope @Component(dependencies = ActivityComponent.class, modules = FragmentModule.class) public interface FragmentComponent { void inject(MyFragment myFragment); }
In diesem Beispiel stellt die Annotation @FragmentScope
sicher, dass die von FragmentModule
bereitgestellten Abhängigkeiten auf den Lebenszyklus des Fragments beschränkt sind.
3. Komponenten
Komponentenabhängigkeiten ermöglichen die Abhängigkeit einer Komponente von einer anderen Komponente und ermöglichen so die Wiederverwendung von Abhängigkeiten. Es gibt zwei Haupttypen von Komponentenabhängigkeiten:
- Anwendungskomponente : Bietet Abhängigkeiten, die in der gesamten Anwendung benötigt werden.
- Aktivitätskomponente : Bietet Abhängigkeiten, die innerhalb einer bestimmten Aktivität benötigt werden.
1. Anwendungskomponente
Definition : Die Anwendungskomponente stellt Abhängigkeiten bereit, die in der gesamten Anwendung benötigt werden. Sie wird normalerweise mit @Singleton
definiert, um sicherzustellen, dass die Abhängigkeiten in der gesamten Anwendung gemeinsam genutzt werden.
Diese Komponente wird für Abhängigkeiten verwendet, die global verfügbar sein müssen, wie z. B. Netzwerkclients, Datenbankinstanzen oder gemeinsame Einstellungen.
Beispiel :
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
In diesem Beispiel ist die ApplicationComponent
für die Bereitstellung Retrofit
und Database
verantwortlich, die von der gesamten Anwendung gemeinsam genutzt werden.
2. Aktivitätskomponente
Definition : Die Aktivitätskomponente stellt Abhängigkeiten bereit, die innerhalb einer bestimmten Aktivität benötigt werden. Sie wird normalerweise mit einem benutzerdefinierten Bereich wie @ActivityScope
versehen, um sicherzustellen, dass die Abhängigkeiten bei jeder Neuerstellung der Aktivität neu erstellt werden.
Diese Komponente wird für Abhängigkeiten verwendet, die spezifisch für eine Aktivität sind, wie etwa Präsentatoren oder Ansichtsmodelle.
Beispiel :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
In diesem Beispiel ist die ActivityComponent
von der ApplicationComponent
abhängig und weist spezifische Abhängigkeiten zur MainActivity
auf.
4. Komponentenabhängigkeiten
Komponentenabhängigkeiten ermöglichen die Abhängigkeit einer Komponente von einer anderen Komponente und ermöglichen so die Wiederverwendung von Abhängigkeiten. Es gibt zwei Haupttypen von Komponentenabhängigkeiten:
- Unterkomponenten : Eine Unterkomponente ist das untergeordnete Element einer anderen Komponente und kann auf die Abhängigkeiten ihres übergeordneten Elements zugreifen.
- Abhängigkeitsattribut : Dadurch kann eine Komponente von einer anderen Komponente abhängig sein, ohne eine Unterkomponente zu sein.
1. Unterkomponenten:
Eine Unterkomponente ist ein untergeordnetes Element einer anderen Komponente und kann auf die Abhängigkeiten ihrer übergeordneten Komponente zugreifen. Unterkomponenten werden innerhalb der übergeordneten Komponente definiert und können deren Umfang erben.
Beispiel :
@ActivityScope @Subcomponent(modules = ActivityModule.class) public interface ActivitySubcomponent { void inject(MainActivity mainActivity); }
In diesem Beispiel ist ActivitySubcomponent
eine Unterkomponente der übergeordneten Komponente und kann auf deren Abhängigkeiten zugreifen.
2. Abhängigkeitsattribut
Dadurch kann eine Komponente von einer anderen Komponente abhängig sein, ohne eine Unterkomponente zu sein. Die abhängige Komponente kann auf die von der übergeordneten Komponente bereitgestellten Abhängigkeiten zugreifen.
Beispiel :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
In diesem Beispiel ist ActivityComponent
von ApplicationComponent
abhängig und kann auf deren Abhängigkeiten zugreifen.
5. Laufzeitbindungen
Laufzeitbindungen in Dagger 2 beziehen sich auf die Bereitstellung von Abhängigkeiten, die zur Laufzeit erstellt und verwaltet werden, basierend auf dem Kontext, in dem sie benötigt werden.
- Anwendungskontext : Wird für Abhängigkeiten verwendet, die so lange bestehen müssen wie die Anwendung.
- Aktivitätskontext : Wird für Abhängigkeiten verwendet, die so lange bestehen müssen wie eine Aktivität.
1. Anwendungskontext
Definition : Der Anwendungskontext ist ein Kontext, der an den Lebenszyklus der gesamten Anwendung gebunden ist. Er wird für Abhängigkeiten verwendet, die so lange bestehen müssen wie die Anwendung selbst.
Abhängigkeiten, die von der gesamten Anwendung gemeinsam genutzt werden und nicht für jede Aktivität oder jedes Fragment neu erstellt werden müssen. Beispiele hierfür sind Netzwerkclients, Datenbankinstanzen und gemeinsame Einstellungen.
Beispiel :
@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(); } }
In diesem Beispiel stellt AppModule
den Anwendungskontext als Singleton-Abhängigkeit bereit. Die Methode provideApplicationContext
stellt sicher, dass der bereitgestellte Kontext an den Lebenszyklus der Anwendung gebunden ist.
2. Aktivitätskontext
Definition : Der Aktivitätskontext ist ein Kontext, der an den Lebenszyklus einer bestimmten Aktivität gebunden ist. Er wird für Abhängigkeiten verwendet, die so lange bestehen müssen wie die Aktivität selbst.
Abhängigkeiten, die spezifisch für eine Aktivität sind und bei jeder Neuerstellung der Aktivität neu erstellt werden sollten. Beispiele hierfür sind Ansichtsmodelle, Präsentatoren und UI-bezogene Abhängigkeiten.
Beispiel :
@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; } }
In diesem Beispiel stellt ActivityModule
den Aktivitätskontext als bereichsbezogene Abhängigkeit bereit. Die Methode provideActivityContext
stellt sicher, dass der bereitgestellte Kontext an den Lebenszyklus der Aktivität gebunden ist.
Verwenden von Runtimebindungen in Komponenten
Um diese Laufzeitbindungen zu verwenden, müssen Sie die entsprechenden Module in Ihre Komponenten einbinden:
Anwendungskomponente :
@Singleton @Component(modules = {AppModule.class, NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); Context getApplicationContext(); }
Aktivitätskomponente :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); Context getActivityContext(); }
Einfügen von Kontexten
Nachdem Sie Ihre Komponenten und Module eingerichtet haben, können Sie die Kontexte nach Bedarf in Ihre Klassen einfügen.
Beispiel in einer Aktivität :
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); } }
In diesem Beispiel erhält MainActivity
sowohl den Aktivitätskontext als auch den Anwendungskontext durch Abhängigkeitseinfügung. Dadurch kann die Aktivität den geeigneten Kontext basierend auf den spezifischen Anforderungen der Abhängigkeiten verwenden.
Beispiel: Verwenden von Dagger 2 in einer Android-Anwendung
Einrichten von Dagger 2
Um Dagger 2 in Ihrem Projekt zu verwenden, müssen Sie Ihrer Datei build.gradle
die folgenden Abhängigkeiten hinzufügen:
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Ersetzen Sie 2.x
durch die neueste Version von Dagger 2.
Schritt 1: Definieren Sie ein Modul
Erstellen Sie ein Modul, um Abhängigkeiten bereitzustellen. Beispielsweise ein NetworkModule
, um eine Retrofit
Instanz bereitzustellen:
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } }
Schritt 2: Definieren einer Komponente
Erstellen Sie eine Komponente, um eine Brücke zwischen dem Modul und den Klassen zu schlagen, die die Abhängigkeiten benötigen:
@Singleton @Component(modules = {NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Schritt 3: Abhängigkeiten einfügen
Verwenden Sie die Komponente, um Abhängigkeiten in Ihre Klassen einzufügen. Beispielsweise in Ihrer 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; } }
Schritt 4: Eingefügte Abhängigkeiten verwenden
Jetzt können Sie die eingefügten Abhängigkeiten in Ihren Klassen verwenden. Beispielsweise in einer 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 // ... } }
Abschluss
Lassen Sie uns dieses Thema zusammenfassen:
- Der Hauptzweck von DI besteht darin, die Kopplung zu lockern und so die Verwaltung von Abhängigkeiten zu vereinfachen.
- Durch die Verwendung von DI können Sie die Codeflexibilität erhöhen und den Testprozess vereinfachen.
- DI ist ein komplexes Thema mit je nach Szenario unterschiedlichen Implementierungen.
- DI weist in verschiedenen Sprachen Besonderheiten auf, die sich auf Ihre Arbeit damit auswirken können.
- Dagger 2 automatisiert den Prozess der Erstellung und Bereitstellung von Abhängigkeiten, reduziert Boilerplate-Code und verbessert die Wartbarkeit.
- Dagger 2 bietet Sicherheit zur Kompilierungszeit und stellt sicher, dass alle Abhängigkeiten erfüllt sind, bevor die Anwendung ausgeführt wird.
- Durch die Generierung von Code zur Kompilierzeit vermeidet Dagger 2 den Leistungsmehraufwand, der mit reflexionsbasierten Lösungen verbunden ist.