Abhängigkeitsinjektion (Data Injection, DI) ist eine Methode, die häufig in der Programmierung verwendet wird und sich gut für die Android-Entwicklung eignet. Wenn Sie die DI-Prinzipien befolgen, legen Sie den Grundstein für eine gute Anwendungsarchitektur.
Die Implementierung einer Abhängigkeitsinjektion bietet Ihnen folgende Vorteile:
- Wiederverwendbarkeit von Code
- Einfache Refaktorierung
- Einfache Tests
Grundlagen der Abhängigkeitsinjektion
Bevor wir speziell auf die Abhängigkeitsinjektion in Android eingehen, bietet diese Seite einen allgemeinen Überblick darüber, wie die Abhängigkeitsinjektion funktioniert.
Was ist Abhängigkeitsinjektion?
Kurse erfordern oft Verweise auf andere Kurse. Beispielsweise benötigt eine Car
-Klasse möglicherweise einen Verweis auf eine Engine
-Klasse. Diese erforderlichen Klassen werden als Abhängigkeiten bezeichnet. In diesem Beispiel ist die Car
-Klasse davon abhängig, ob eine Instanz der Engine
-Klasse ausgeführt wird.
Eine Klasse hat drei Möglichkeiten, ein benötigtes Objekt abzurufen:
- Die Klasse konstruiert die Abhängigkeit, die sie benötigt. Im obigen Beispiel würde
Car
eine eigene Instanz vonEngine
erstellen und initialisieren. - Hol sie dir von einem anderen Ort. Einige Android APIs wie
Context
-Getter undgetSystemService()
funktionieren auf diese Weise. - Sie muss als Parameter angegeben werden. Die Anwendung kann diese Abhängigkeiten bereitstellen, wenn die Klasse erstellt wird, oder sie an die Funktionen übergeben, die die einzelnen Abhängigkeiten benötigen. Im Beispiel oben würde der Konstruktor
Car
Engine
als Parameter empfangen.
Die dritte Option ist die Abhängigkeitsinjektion. Bei diesem Ansatz stellen Sie die Abhängigkeiten einer Klasse bereit, anstatt sie von der Klasseninstanz selbst abrufen zu lassen.
Hier ein Beispiel. Ohne Abhängigkeitsinjektion kann die Darstellung eines Car
, der seine eigene Engine
-Abhängigkeit im Code erstellt, so aussehen:
Kotlin
class Car { private val engine = Engine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class Car { private Engine engine = new Engine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }
![Autoklasse ohne Abhängigkeitsinjektion](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-no-di.png?authuser=6&hl=de)
Dies ist kein Beispiel für eine Abhängigkeitsinjektion, da die Car
-Klasse ihre eigene Engine
erstellt. Das kann aus folgenden Gründen problematisch sein:
Car
undEngine
sind eng miteinander verbunden: Eine Instanz vonCar
verwendet einen Typ vonEngine
. Es können keine abgeleiteten Klassen oder alternativen Implementierungen verwendet werden. Wenn dieCar
ihre eigeneEngine
erstellen würde, müssten Sie zwei Typen vonCar
erstellen, anstatt dieselbeCar
für Engines vom TypGas
undElectric
wiederzuverwenden.Die harte Abhängigkeit von
Engine
erschwert das Testen.Car
verwendet eine echte Instanz vonEngine
. Daher können SieEngine
nicht mit einem Test-Double für verschiedene Testläufe ändern.
Wie sieht der Code mit Abhängigkeitsinjektion aus? Anstatt dass jede Instanz von Car
bei der Initialisierung ein eigenes Engine
-Objekt erstellt, empfängt sie ein Engine
-Objekt als Parameter in ihrem Konstruktor:
Kotlin
class Car(private val engine: Engine) { fun start() { engine.start() } } fun main(args: Array) { val engine = Engine() val car = Car(engine) car.start() }
Java
class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Engine engine = new Engine(); Car car = new Car(engine); car.start(); } }
![Autoklasse mit Abhängigkeitsinjektion](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-di.png?authuser=6&hl=de)
Die Funktion main
verwendet Car
. Da Car
von Engine
abhängt, erstellt die Anwendung eine Instanz von Engine
und verwendet diese dann, um eine Instanz von Car
zu erstellen. Die Vorteile dieses DI-basierten Ansatzes sind:
Wiederverwendbarkeit von
Car
. Sie können verschiedene Implementierungen vonEngine
anCar
übergeben. Beispielsweise können Sie eine neue abgeleitete Klasse vonEngine
namensElectricEngine
definieren, dieCar
verwenden soll. Wenn Sie DI verwenden, müssen Sie lediglich eine Instanz der aktualisierten abgeleiteten KlasseElectricEngine
übergeben.Car
funktioniert dann weiterhin ohne weitere Änderungen.Einfaches Testen von
Car
. Sie können Test-Doubles bestehen, um Ihre verschiedenen Szenarien zu testen. Sie können beispielsweise ein Test-Double vonEngine
mit dem NamenFakeEngine
erstellen und für verschiedene Tests konfigurieren.
Es gibt zwei Möglichkeiten, eine Abhängigkeitsinjektion in Android durchzuführen:
Konstruktoreinschleusung. Dies ist die oben beschriebene Weise. Sie übergeben die Abhängigkeiten einer Klasse an ihren Konstruktor.
Field Injection (oder Setter Injection) Bestimmte Android-Framework-Klassen wie Aktivitäten und Fragmente werden vom System instanziiert, sodass eine Konstruktor-Injektion nicht möglich ist. Mit Field Injection werden Abhängigkeiten instanziiert, nachdem die Klasse erstellt wurde. Der Code würde so aussehen:
Kotlin
class Car { lateinit var engine: Engine fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.engine = Engine() car.start() }
Java
class Car { private Engine engine; public void setEngine(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.setEngine(new Engine()); car.start(); } }
Automatisierte Abhängigkeitsinjektion
Im vorherigen Beispiel haben Sie die Abhängigkeiten der verschiedenen Klassen selbst erstellt, bereitgestellt und verwaltet, ohne sich auf eine Bibliothek zu verlassen. Dies wird als Abhängigkeitsinjektion von Hand oder manuelle Abhängigkeitsinjektion bezeichnet. Im Car
-Beispiel gab es nur eine Abhängigkeit, aber mehr Abhängigkeiten und Klassen können das manuelle Einschleusen von Abhängigkeiten mühsamer machen. Die manuelle Abhängigkeitsinjektion bringt außerdem mehrere Probleme mit sich:
Bei großen Anwendungen kann es eine große Menge an Boilerplate-Code erfordern, um alle Abhängigkeiten zu übernehmen und richtig zu verbinden. Wenn Sie in einer mehrschichtigen Architektur ein Objekt für eine oberste Ebene erstellen möchten, müssen Sie alle Abhängigkeiten der darunterliegenden Ebenen angeben. Ein konkretes Beispiel: Für den Bau eines echten Autos benötigen Sie möglicherweise einen Motor, ein Getriebe, ein Fahrwerk und andere Teile. Ein Motor wiederum braucht Zylinder und Zündkerzen.
Wenn Sie keine Abhängigkeiten erstellen können, bevor Sie sie übergeben, z. B. wenn Sie verzögerte Initialisierungen verwenden oder den Umfang von Objekten für Abläufe Ihrer Anwendung festlegen, müssen Sie einen benutzerdefinierten Container (oder eine Grafik mit Abhängigkeiten) schreiben und verwalten, der die Lebensdauer Ihrer Abhängigkeiten im Arbeitsspeicher verwaltet.
Es gibt Bibliotheken, die dieses Problem lösen, indem sie das Erstellen und Bereitstellen von Abhängigkeiten automatisieren. Sie lassen sich in zwei Kategorien einteilen:
Reflexionsbasierte Lösungen, die Abhängigkeiten zur Laufzeit verbinden
Statische Lösungen, die den Code generieren, um Abhängigkeiten bei der Kompilierung zu verbinden
Dagger ist eine beliebte Abhängigkeitsinjektionsbibliothek für Java, Kotlin und Android, die von Google gepflegt wird. Dagger erleichtert die Verwendung von DI in Ihrer Anwendung, indem das Diagramm der Abhängigkeiten für Sie erstellt und verwaltet wird. Sie bietet vollständig statische Abhängigkeiten und Kompilierungszeit, um viele der Entwicklungs- und Leistungsprobleme von Lösungen auf Basis von Reflexionen wie Guice zu lösen.
Alternativen zur Abhängigkeitsinjektion
Eine Alternative zur Abhängigkeitsinjektion ist die Verwendung einer Service Locator. Das Service Locator-Designmuster verbessert auch die Entkopplung von Klassen von konkreten Abhängigkeiten. Sie erstellen eine Klasse namens Service Locator, die Abhängigkeiten erstellt und speichert und diese dann bei Bedarf bereitstellt.
Kotlin
object ServiceLocator { fun getEngine(): Engine = Engine() } class Car { private val engine = ServiceLocator.getEngine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class ServiceLocator { private static ServiceLocator instance = null; private ServiceLocator() {} public static ServiceLocator getInstance() { if (instance == null) { synchronized(ServiceLocator.class) { instance = new ServiceLocator(); } } return instance; } public Engine getEngine() { return new Engine(); } } class Car { private Engine engine = ServiceLocator.getInstance().getEngine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }
Das Service Locator-Muster unterscheidet sich von der Abhängigkeitsinjektion in der Art und Weise, wie die Elemente verarbeitet werden. Mit dem Service Locator-Muster können Klassen das Einschleusen von Objekten steuern und anfordern. Bei der Abhängigkeitsinjektion hat die App die Kontrolle und schleust die erforderlichen Objekte proaktiv ein.
Im Vergleich zur Abhängigkeitsinjektion:
Die Sammlung von Abhängigkeiten, die für eine Dienstsuche erforderlich sind, erschwert das Testen von Code, da alle Tests mit derselben globalen Dienstsuche interagieren müssen.
Abhängigkeiten werden in der Klassenimplementierung codiert, nicht in der API-Oberfläche. Daher ist es schwieriger, zu erkennen, was ein Kurs von außen braucht. Änderungen an
Car
oder an den in der Dienstsuche verfügbaren Abhängigkeiten können deshalb zu Laufzeit- oder Testfehlern führen, da Verweise fehlschlagen.Die Verwaltung der Lebensdauer von Objekten ist schwieriger, wenn Sie den Umfang auf etwas anderes als die Lebensdauer der gesamten Anwendung beschränken möchten.
Hilt in Android-Apps verwenden
Hilt ist die von Jetpack empfohlene Bibliothek für Abhängigkeitsinjektionen in Android. Hilt definiert eine Standardmethode zur Durchführung von DI in Ihrer Anwendung. Dazu stellt es Container für jede Android-Klasse in Ihrem Projekt bereit und verwaltet deren Lebenszyklen automatisch für Sie.
Hilt baut auf der beliebten DI-Bibliothek Dagger auf, um von der Kompilierungszeitrichtigkeit, Laufzeitleistung, Skalierbarkeit und Android Studio-Unterstützung zu profitieren, die Dagger bietet.
Weitere Informationen zu Hilt finden Sie unter Abhängigkeitsinjektion mit Hilt.
Fazit
Die Abhängigkeitsinjektion bietet Ihrer Anwendung die folgenden Vorteile:
Wiederverwendbarkeit von Klassen und Entkopplung von Abhängigkeiten: Es ist einfacher, Implementierungen einer Abhängigkeit auszutauschen. Die Wiederverwendung von Code wird aufgrund der Umkehrung der Kontrolle verbessert. Klassen steuern nicht mehr, wie ihre Abhängigkeiten erstellt werden, sondern arbeiten stattdessen mit jeder Konfiguration.
Einfache Refaktorierung: Die Abhängigkeiten werden zu einem überprüfbaren Teil der API-Oberfläche, sodass sie bei der Objekterstellung oder beim Kompilieren überprüft werden können, anstatt sie als Implementierungsdetails zu verbergen.
Einfache Tests: Eine Klasse verwaltet ihre Abhängigkeiten nicht. Wenn du sie also testest, kannst du verschiedene Implementierungen übergeben, um alle deine verschiedenen Fälle zu testen.
Um die Vorteile der Abhängigkeitsinjektion vollständig zu verstehen, sollten Sie sie manuell in Ihrer App ausprobieren, wie unter Manuelle Abhängigkeitsinjektion beschrieben.
Weitere Informationen
Weitere Informationen zur Abhängigkeitsinjektion finden Sie in den folgenden zusätzlichen Ressourcen.