Android Coden
Android 4 min lesen

Feature-Module: Screens und Logik eines Features klar abgrenzen

Feature-Module fassen Screen, Logik und Datenzugriff eines Features zusammen. Klare APIs halten die Modulgrenze dauerhaft sauber.

Wenn eine Android-App wächst, verstreuen sich Screen-Code, Use-Cases und Datenzugriffe schnell über das gesamte Projekt. Feature-Module sind die Antwort der offiziellen Android-Architekturrichtlinien auf dieses Problem: Sie bündeln alles, was zu einem einzigen Feature gehört – UI, Domain-Logik, Datenzugriff – in einem eigenen Gradle-Modul und verbergen die Interna hinter einer schmalen, stabilen API nach außen.

Was ist das?

Ein Feature-Modul ist ein dediziertes Gradle-Modul, das den vollständigen vertikalen Schnitt eines Features abdeckt. „Vertikal” bedeutet: von der Compose-Screen-Implementierung über ViewModels und Use-Cases bis hin zu Repository-Aufrufen – alles, was ausschließlich zu diesem Feature gehört, liegt im selben Modul.

Feature-Module sind nicht zu verwechseln mit Dynamic Feature Modules, die Google Play für die bedarfsgesteuerte Auslieferung von App-Teilen nutzt. Hier geht es nicht um App-Delivery, sondern um Architektur: Zuständigkeiten klar zu trennen, damit Teams unabhängig arbeiten können und der Gradle-Build Änderungen inkrementell kompiliert.

Im offiziellen Android-Architekturmodell sitzt das Feature-Modul über den gemeinsamen Core-Modulen – zum Beispiel :core:ui, :core:domain, :core:data – und unter dem :app-Modul, das nur noch die Navigation verdrahtet und den App-Entry-Point enthält. Diese Schichtung erzwingt gerichtete Abhängigkeiten: Features kennen die Core-Schicht, aber nicht einander.

Wie funktioniert es?

Gradle-Abhängigkeiten

Jedes Feature-Modul deklariert Abhängigkeiten ausschließlich nach unten – zu Core-Modulen, nie zu anderen Feature-Modulen. Ein typisches build.gradle.kts für ein Login-Feature sieht so aus:

// feature/login/build.gradle.kts
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.hilt.android)
}

dependencies {
    implementation(projects.core.ui)
    implementation(projects.core.domain)
    implementation(projects.core.data)
}

Das Schlüsselmerkmal: :feature:feed und :feature:settings tauchen hier nicht auf. Feature-Module kennen sich nicht.

Kapselung mit internal

Kotlins internal-Sichtbarkeit ist das wichtigste Werkzeug zur Modulabgrenzung. Alles, was nur innerhalb des Feature-Moduls benötigt wird – ViewModels, konkrete Use-Case-Implementierungen, interne State-Klassen – wird internal deklariert. Nach außen sichtbar ist nur das, was für die Navigation oder Dependency-Injection nötig ist.

// Öffentlich: nur der Hilt-Einstiegspunkt und der NavGraph-Erweiterungspunkt
@HiltViewModel
internal class LoginViewModel @Inject constructor(
    private val loginUseCase: LoginUseCase
) : ViewModel() {
    // vollständig intern
}

// Öffentliche Funktion, die der NavHost in :app registriert
fun NavGraphBuilder.loginGraph(onLoginSuccess: () -> Unit) {
    composable(LoginDestination.route) {
        LoginScreen(onLoginSuccess = onLoginSuccess)
    }
}

Da Feature-Module nicht voneinander wissen dürfen, läuft die Navigation über einen gemeinsamen Vertrag. In der Praxis registriert das :app-Modul alle Feature-NavGraphen im zentralen NavHost und übergibt Callbacks von außen. Die Features selbst definieren keine Abhängigkeit auf das Navigationsziel eines anderen Features – sie rufen den Callback auf und lassen :app entscheiden, was als Nächstes kommt.

In der Praxis

Eine typische Modulstruktur für eine mittlere App könnte so aussehen:

:app
:feature:login
:feature:feed
:feature:profile
:core:ui
:core:domain
:core:data
:core:testing

Das :feature:login-Modul exponiert genau zwei Dinge nach außen: die loginGraph()-Erweiterungsfunktion für den NavHost und ein Hilt-Modul, das etwaige öffentliche Abhängigkeiten bereitstellt. Alles andere – LoginViewModel, LoginScreen, interne Hilfsfunktionen – bleibt internal und ist aus anderen Modulen schlicht nicht erreichbar.

Typische Stolperfalle: Feature-Module beginnen, sich gegenseitig direkt zu importieren. Sobald :feature:feed eine Datenklasse aus :feature:login referenziert, entsteht eine ungewollte Kopplung. Spätestens wenn zwei Features sich wechselseitig importieren, bricht der Gradle-Build mit einem Zyklus-Fehler ab. Die Lösung ist immer dieselbe: Den gemeinsamen Code in ein Core-Modul verschieben – nie das Feature als Bibliothek für andere Features missbrauchen.

Testbarkeit: Da die Interna eines Feature-Moduls nicht von außen erreichbar sind, schreibst du Unit-Tests direkt im selben Modul. Das :core:testing-Modul stellt Fakes und Test-Doubles bereit, auf die jedes Feature zugreifen kann. Für CI empfehlen sich Tools wie Dependency Guard oder Module-Graph-Assertions, die bei jedem Pull Request prüfen, ob neue Importe die Modulgrenze verletzen.

Fazit

Feature-Module sind das architektonische Fundament skalierbarer Android-Apps. Sie machen Zuständigkeiten sichtbar, ermöglichen paralleles Arbeiten in Teams und beschleunigen den inkrementellen Gradle-Build, weil nur geänderte Module neu kompiliert werden. Den echten Nutzen spürst du, indem du ein bestehendes Feature in deinem Projekt in ein eigenes Modul extrahierst: Welche Abhängigkeiten musst du dabei auflösen? Wo verstecken sich implizite Querverweise zwischen Features? Die Antworten zeigen dir, wie gut deine aktuelle Architektur wirklich getrennt ist – und wo der nächste sinnvolle Refactoring-Schritt liegt.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

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