Abhängigkeitsinjektion in Android

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:

  1. Die Klasse konstruiert die Abhängigkeit, die sie benötigt. Im obigen Beispiel würde Car eine eigene Instanz von Engine erstellen und initialisieren.
  2. Hol sie dir von einem anderen Ort. Einige Android APIs wie Context-Getter und getSystemService() funktionieren auf diese Weise.
  3. 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

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 und Engine sind eng miteinander verbunden: Eine Instanz von Car verwendet einen Typ von Engine. Es können keine abgeleiteten Klassen oder alternativen Implementierungen verwendet werden. Wenn die Car ihre eigene Engine erstellen würde, müssten Sie zwei Typen von Car erstellen, anstatt dieselbe Car für Engines vom Typ Gas und Electric wiederzuverwenden.

  • Die harte Abhängigkeit von Engine erschwert das Testen. Car verwendet eine echte Instanz von Engine. Daher können Sie Engine 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

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 von Engine an Car übergeben. Beispielsweise können Sie eine neue abgeleitete Klasse von Engine namens ElectricEngine definieren, die Car verwenden soll. Wenn Sie DI verwenden, müssen Sie lediglich eine Instanz der aktualisierten abgeleiteten Klasse ElectricEngine ü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 von Engine mit dem Namen FakeEngine 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.

Produktproben