Android Coden
Android 4 min lesen

Sinn und Zweck der Modularisierung

Modularisierung teilt Android-Projekte in eigenständige Module auf. Wann das sinnvoll ist und welche Grenzen du ziehen solltest.

Ab einem gewissen Projektumfang wird der monolithische Ansatz zur Bremse: Jede Änderung kompiliert alles neu, jedes Team arbeitet auf denselben Dateien, und die Architektur verschwimmt schleichend. Modularisierung löst dieses Problem, indem sie zusammengehörigen Code in eigenständige Gradle-Module verpackt – mit definierten Grenzen, eigener öffentlicher API und klar zugeschnittenem Verantwortungsbereich.

Was ist das?

Modularisierung ist die Praxis, eine Android-App in mehrere unabhängige Gradle-Module aufzuteilen, anstatt den gesamten Code in einem einzigen :app-Modul zu bündeln. Jedes Modul ist ein eigenständiges Build-Artefakt mit eigenen Quelldateien, eigener build.gradle-Konfiguration und einer kontrollierten öffentlichen Schnittstelle.

Im Android-Ökosystem unterscheidet man grob drei Modultypen:

  • Feature-Module kapseln eine vollständige Nutzerfunktion, etwa Login, Suche oder Warenkorb.
  • Library-Module stellen domänenübergreifend genutzten Code bereit, zum Beispiel Netzwerklogik, Datenbankzugriff oder Design-System-Komponenten.
  • App-Module fungieren als Einstiegspunkt – sie kennen alle anderen Module, wissen aber nichts über deren interne Implementierung.

Das Ziel ist nicht Modularisierung um ihrer selbst willen, sondern eine Struktur, die dem Wachstum des Projekts und des Teams gerecht wird. Solange ein Projekt klein ist und ein einzelner Entwickler alle Teile überblickt, reicht eine saubere Paket- und Schichtstruktur innerhalb eines Moduls völlig aus.

Wie funktioniert es?

Die zentrale Idee ist das Dependency-Graph-Prinzip: Abhängigkeiten zwischen Modulen dürfen nur in eine Richtung zeigen. Feature-Module dürfen Library-Module verwenden, aber nie andere Feature-Module direkt importieren – sonst entsteht eine zirkuläre Abhängigkeit, die Gradle ablehnt und die das Architekturprinzip der klaren Grenzen untergräbt.

Jedes Modul exponiert nur das, was andere wissen müssen. In Kotlin bedeutet das: öffentliche Klassen und Funktionen gehören zur Modul-API, der Rest bleibt internal oder private. Diese Kapselung ist der entscheidende Unterschied zu einer reinen Paketstruktur innerhalb eines Monolithen – dort gibt es keine erzwungene Grenze, nur Konventionen.

Für den Build-Gewinn sorgt das Incremental-Build-Verhalten von Gradle: Ändert sich ein Feature-Modul, müssen nur es und seine direkten Abhängigen neu kompiliert werden. Bei konsequenter Modularisierung kann das die Compile-Zeit im Entwicklungsalltag halbieren oder mehr, besonders in größeren Codebasen mit parallelen CI-Pipelines.

Die offizielle Architektur-Empfehlung von Google gliedert Module nach drei Kriterien:

  1. Ownership – Welches Team oder welche Person ist verantwortlich?
  2. Stabilität – Wie häufig ändert sich der Code?
  3. Testbarkeit – Lässt sich das Modul isoliert testen?

Liegen mehrere dieser Kriterien an einer Codegrenze vor, ist das ein starkes Signal, dort ein neues Modul zu schneiden.

In der Praxis

Wann du modularisierst – und wann nicht

Die häufigste Falle: Entwickler modularisieren zu früh. Ein frisch gestartetes Projekt mit einem Entwickler und drei Screens braucht keine fünf Module. Der Overhead – separate build.gradle-Dateien, Gradle-Versionsabgleich, Navigation zwischen Modulen – überwiegt den Nutzen, solange kein zweites Team und kein spürbarer Build-Schmerz existiert.

Die Faustregel lautet: Teile Code auf, wenn die Aufteilung Build-Geschwindigkeit, Ownership oder Lesbarkeit verbessert. Trifft keiner dieser drei Punkte zu, genügt eine saubere Paket- und Schichtstruktur innerhalb eines einzigen Moduls.

Struktur eines einfachen Multi-Module-Projekts

MyApp/
├── app/                  # :app – Einstiegspunkt, Activity, Navigation
├── feature/
│   ├── login/            # :feature:login
│   └── dashboard/        # :feature:dashboard
└── core/
    ├── network/          # :core:network – Retrofit, OkHttp
    ├── database/         # :core:database – Room
    └── ui/               # :core:ui – gemeinsame Compose-Komponenten

In der build.gradle.kts des :feature:login-Moduls sieht eine Abhängigkeit auf das Core-Netzwerk-Modul so aus:

dependencies {
    implementation(project(":core:network"))
    implementation(project(":core:ui"))
}

Das :feature:login-Modul kennt :core:network, aber :core:network weiß nichts von :feature:login. Die Abhängigkeit zeigt nur nach innen, nie zurück nach außen.

Typische Stolperfalle: Shared State über Modulgrenzen

Ein verbreiteter Fehler ist es, einen globalen ViewModel oder ein Application-Singleton aus dem :app-Modul direkt in Feature-Module zu injizieren. Dadurch entsteht eine versteckte Abhängigkeit, die die sauber definierten Modulgrenzen aushöhlt. Stattdessen kommunizieren Module über klar definierte Schnittstellen – etwa via Hilt-bereitgestellte Interfaces, geteilte Datenmodelle in einem :core:model-Modul oder Navigation-Arguments. Jede Abkürzung über Modulgrenzen hinweg zahlt sich kurzfristig aus, macht das Projekt aber langfristig schwerer testbar und refaktorierbar.

Fazit

Modularisierung ist kein Selbstzweck, sondern ein Werkzeug, das greift, sobald Projekte groß genug sind, um von schnelleren Builds, klaren Teamgrenzen und isolierbaren Tests zu profitieren. Prüfe in deinem aktuellen Projekt: Gibt es Codeblöcke, die mehrere Features gemeinsam nutzen? Gibt es Bereiche, die ein anderes Team besser isoliert halten könnte? Extrahiere probehalber die Datenbankschicht in ein :core:database-Modul und beobachte, wie sich Build-Zeit und Testbarkeit verändern. Dieser eine konkrete Schritt macht das Konzept greifbar und zeigt, ob weitere Modularisierung für dein Projekt lohnt.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

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