Android Coden
Android 4 min lesen

Abhängigkeitsrichtung zwischen Modulen

Zirkuläre Abhängigkeiten zwischen Modulen lähmen Builds und Tests. Dieser Artikel zeigt, warum Abhängigkeiten immer in eine Richtung fließen müssen.

Sobald ein Android-Projekt wächst, teilt man den Code in mehrere Gradle-Module auf: ein App-Modul, Feature-Module, gemeinsam genutzte Bibliotheken. Dabei entsteht zwangsläufig die Frage, welches Modul von welchem anderen abhängen darf. Die Antwort legt fest, ob dein Projekt langfristig wartbar bleibt – oder ob jede neue Funktion den Build langsamer und das Testen schwieriger macht.

Was ist das?

Module Dependency Direction bezeichnet die Regel, dass Abhängigkeiten zwischen Gradle-Modulen stets in eine einzige Richtung zeigen müssen – von höherstufigen Modulen zu niedrigerstufigen, niemals zurück. Das Ergebnis ist ein gerichteter azyklischer Graph (Directed Acyclic Graph, DAG): Jeder Knoten steht für ein Modul, jede Kante für eine implementation- oder api-Abhängigkeit, und es gibt keinen Pfad, der zum Ausgangspunkt zurückführt.

Das Gegenteil ist eine zirkuläre Abhängigkeit: Modul A importiert Modul B, und Modul B importiert gleichzeitig Modul A. Gradle bricht in diesem Fall den Build sofort mit einem Fehler ab. Das ist jedoch nur das sichtbare Symptom eines tieferliegenden Designproblems – die beiden Module sind so stark miteinander verkoppelt, dass sie weder unabhängig voneinander kompiliert noch isoliert getestet werden können.

Im Kontext der offiziellen Android-Architektur-Empfehlungen bilden Module die physischen Grenzen zwischen Schichten. Eine saubere Abhängigkeitsrichtung sorgt dafür, dass das Dependency-Inversion-Prinzip eingehalten wird und Features tatsächlich unabhängig voneinander bleiben – was direkte Auswirkungen auf Build-Geschwindigkeit, Testbarkeit und Skalierbarkeit im Team hat.

Wie funktioniert es?

Gradle repräsentiert alle Projektabhängigkeiten intern als DAG. Beim Build traversiert Gradle diesen Graphen und kompiliert abhängige Module erst dann, wenn ihre Abhängigkeiten fertig gebaut sind. Entsteht ein Zyklus, kann Gradle keinen Startpunkt für die Traversierung finden – daher der sofortige Fehler.

Die empfohlene Schichtung für Android-Projekte sieht so aus:

  • :app – das Launcher-Modul; hängt von Feature-Modulen und bei Bedarf direkt von Core-Modulen ab.
  • :feature:* – je ein Modul pro Anwendungsfall (z. B. :feature:login, :feature:settings); hängt ausschließlich von Core-Modulen ab.
  • :core:* – gemeinsam genutzte Bibliotheken wie :core:network, :core:ui, :core:data; hängt von keinen Feature-Modulen ab.

Diese Hierarchie erzwingt von Natur aus einen azyklischen Graphen. Das :app-Modul darf :core:network importieren, aber :core:network darf niemals :feature:login importieren. Die Richtung ist damit eindeutig und für alle Beteiligten lesbar.

Dieselbe Regel gilt innerhalb der Core-Schicht. :core:data darf :core:network verwenden, aber nicht umgekehrt. Sobald zwei Core-Module gegenseitig voneinander abhängen, ist das ein starkes Signal, dass entweder eine gemeinsame Abstraktionsschicht (z. B. :core:common) fehlt oder die Verantwortlichkeiten neu geschnitten werden müssen.

Warum das für Tests entscheidend ist

Wenn Module keine Zyklen enthalten, lässt sich jedes Modul isoliert testen – in einem kleinen, schnellen Unit-Test, der keine anderen Features hochfährt. In einer zyklischen Struktur muss der Test zwangsläufig das gesamte Geflecht mitladen, was Build-Zeiten verlängert und die Fehleroberfläche vergrößert. Für Continuous Integration ist das besonders schmerzhaft: Lange Build-Zeiten verlangsamen den gesamten Feedback-Zyklus.

In der Praxis

Ein typischer build.gradle.kts-Ausschnitt für ein Feature-Modul sieht so aus:

// :feature:settings/build.gradle.kts
dependencies {
    implementation(project(":core:ui"))
    implementation(project(":core:data"))

    // FALSCH – Feature-Module dürfen nicht voneinander abhängen:
    // implementation(project(":feature:login"))
}

Wenn :feature:settings Logik aus :feature:login benötigt – etwa eine Funktion zur Sitzungsprüfung –, ist die korrekte Lösung, diese Logik in ein Core-Modul zu verschieben, z. B. :core:auth. Beide Feature-Module binden dann :core:auth ein, ohne voneinander zu wissen.

Typische Stolperfalle – Navigation zwischen Features: Ein Feature soll direkt zu einem anderen Feature navigieren und ist versucht, project(":feature:other") einzubinden. Das führt entweder zu einer zirkulären Abhängigkeit oder zu einer versteckten Kopplung. Die empfohlene Lösung ist ein Navigationsvertrag über ein Interface in einem Core-Modul:

// :core:navigation/src/main/kotlin/Navigation.kt
interface FeatureNavigator {
    fun openSettings()
    fun openLogin()
}

Das :app-Modul implementiert dieses Interface und injiziert es via Dependency Injection in die Feature-Module. Die Features rufen nur das Interface auf – sie wissen nichts voneinander.

So erkennst du Probleme frühzeitig: Führe ./gradlew dependencies --project :feature:settings aus und prüfe den Abhängigkeitsbaum. Taucht dort ein anderes Feature-Modul auf, hast du eine unerwünschte Kopplung eingebaut. Viele Teams ergänzen in der CI-Pipeline einen einfachen Lint-Check oder einen Gradle-Projektgraphen-Test, der alle Feature-zu-Feature-Kanten als Fehler markiert.

Fazit

Eine korrekte Abhängigkeitsrichtung ist keine akademische Übung, sondern der Unterschied zwischen einem Projekt, das mit jedem Sprint leichter zu ändern ist, und einem, das jeden Refactor zur Baustelle macht. Öffne jetzt die build.gradle.kts-Dateien deiner Feature-Module und stelle sicher, dass sie ausschließlich Core-Module importieren. Schreibe anschließend einen einfachen Unit-Test im Feature-Modul und verifiziere, dass er ohne Abhängigkeit auf andere Feature-Module kompiliert und läuft. Dieser Test ist dein verlässlichstes Signal, dass die Modulgrenzen tatsächlich sauber gezogen sind – und eine Gewohnheit, die dein gesamtes Team von dieser Arbeit profitieren lässt.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.