Große Android-Codebases skalieren
Große Android-Projekte brauchen klare Modulgrenzen und schnelle Builds. Du lernst, wie Struktur, Ownership und Feedbackzeiten zusammenhängen.
Wenn eine Android-App wächst, reicht es nicht mehr, dass der Code nur funktioniert. Du brauchst eine Struktur, in der Änderungen lokal bleiben, Builds schnell genug sind und Teams wissen, wofür sie verantwortlich sind. Skalierung einer großen Android-Codebase bedeutet genau das: Die App soll trotz vieler Features, Module und Entwickler weiterhin wartbar bleiben.
Was ist das?
Scaling Large Android Codebases beschreibt den Umgang mit Android-Projekten, die über eine einzelne App-Struktur hinausgewachsen sind. Typisch sind mehrere Feature-Teams, viele Gradle-Module, gemeinsame Design-Systeme, Core-Libraries, Legacy-Code und unterschiedliche Release-Risiken.
Das Ziel ist nicht, möglichst viele Module zu erzeugen. Das Ziel ist, Änderungsbereiche zu trennen. Wenn du am Login arbeitest, sollte möglichst wenig vom Warenkorb, Profil oder Payment neu kompiliert, getestet oder verstanden werden müssen. Diese Trennung verbessert Wartbarkeit, Build Performance und Code-Review-Qualität.
Im modernen Android-Kontext hängt das eng mit Architektur zusammen. Kotlin, Jetpack, Compose, ViewModels, Repository-Schichten und Dependency Injection helfen nur dann dauerhaft, wenn sie in klare Grenzen eingebettet sind. Eine gute Codebase macht sichtbar, welche Schicht UI enthält, wo Fachlogik lebt, welche APIs stabil sind und welches Team ein Modul besitzt.
Das mentale Modell für Anfänger: Stell dir die App nicht als einen großen Ordner vor, sondern als Stadtplan. Es gibt Wohnbereiche, Straßen, öffentliche Schnittstellen und private Räume. Gute Skalierung heißt, dass du dich bewegen kannst, ohne durch jede Wohnung laufen zu müssen.
Wie funktioniert es?
Der wichtigste Hebel ist Modularisierung. Ein Modul bündelt Code, Ressourcen und Abhängigkeiten. Häufige Modularten sind :app, :feature:profile, :feature:checkout, :core:network, :core:database oder :designsystem. Feature-Module enthalten fachliche Screens und Abläufe. Core-Module enthalten wiederverwendbare Infrastruktur. Shared-UI-Module enthalten Komponenten, Themes und Icons.
Wichtig ist die Richtung der Abhängigkeiten. Ein Feature darf meist Core-APIs nutzen, aber Core sollte kein konkretes Feature kennen. Sonst entsteht ein Kreis aus Abhängigkeiten, der Builds verlangsamt und Änderungen riskanter macht. In Compose-Projekten bedeutet das zum Beispiel: Ein Feature kann ProfileScreen() anbieten, aber andere Module sollten nicht ungeprüft interne Composables, Preview-Daten oder Navigation-Details verwenden.
Build Performance entsteht aus vielen kleinen Entscheidungen. Gradle kann Module getrennt kompilieren und Aufgaben wiederverwenden, wenn Abhängigkeiten sauber sind. Wenn dagegen jedes Modul fast alles importiert, verliert die Aufteilung ihren Wert. Auch Annotation Processing, große Ressourcenmodule, unnötige api-Abhängigkeiten und globale Build-Skripte können Feedbackzeiten verschlechtern.
Ownership ergänzt die technische Struktur. Ein Modul sollte eine klare verantwortliche Gruppe haben. Diese Gruppe pflegt öffentliche APIs, prüft Reviews und entscheidet über Migrationen. Ohne Ownership werden zentrale Module schnell zu Sammelstellen für alles, was gerade irgendwo gebraucht wird. Dann wächst die Kopplung, obwohl die Projektstruktur auf den ersten Blick ordentlich aussieht.
In der Praxis
Eine brauchbare Regel lautet: Schneide Module entlang stabiler fachlicher Grenzen, nicht entlang einzelner Android-Klassen. Ein Modul :feature:settings ist oft sinnvoller als getrennte Module für activities, viewmodels und repositories, weil ein Feature meist gemeinsam geändert, getestet und verstanden wird.
Ein vereinfachtes Beispiel für Gradle-Abhängigkeiten:
// feature/profile/build.gradle.kts
plugins {
id("com.android.library")
kotlin("android")
}
android {
namespace = "de.androidcoden.profile"
}
dependencies {
implementation(project(":core:model"))
implementation(project(":core:data"))
implementation(project(":designsystem"))
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.compose.material3:material3:1.3.1")
}
Hier nutzt :feature:profile gemeinsame Modelle, Datenzugriff und UI-Komponenten. Das Feature bleibt aber Besitzer seiner Screens und seines ViewModels. Ein anderes Feature sollte nicht direkt interne Klassen aus profile verwenden. Wenn es Daten aus dem Profil braucht, ist eine kleine öffentliche Schnittstelle besser als ein Zugriff auf interne Implementierung.
Eine typische Stolperfalle ist das Modul :core:common. Am Anfang wirkt es praktisch. Nach einigen Monaten enthält es Extensions, Mapper, UI-Helfer, Testdaten, Formatierung und halbe Fachlogik. Dadurch hängt plötzlich fast jedes Modul daran. Eine Änderung in common kann dann viele Builds und Tests auslösen. Besser ist es, präzisere Module zu schneiden, etwa :core:datetime, :core:analytics oder :core:result, sofern der Nutzen klar ist.
Prüfe bei Code-Reviews drei Fragen: Muss dieses Modul die neue Abhängigkeit wirklich kennen? Wird eine öffentliche API stabiler oder größer? Verändert die Änderung die Build-Zeit vieler anderer Module? Wenn du diese Fragen regelmäßig stellst, erkennst du Skalierungsprobleme früher.
Du kannst dein Verständnis praktisch testen, indem du ein kleines Beispielprojekt in drei Module aufteilst: :app, :feature:notes und :core:model. Ändere danach nur eine UI-Datei im Feature und beobachte im Build-Output, welche Gradle-Tasks laufen. Wenn sehr viele unbeteiligte Module neu gebaut werden, ist das ein Hinweis auf zu breite Abhängigkeiten.
Fazit
Große Android-Codebases skalierst du nicht durch Ordnerkosmetik, sondern durch klare Modulgrenzen, kontrollierte Abhängigkeiten, schnelle Feedbackschleifen und sichtbare Ownership. Übe das an einem bestehenden Projekt: Zeichne die Modulabhängigkeiten auf, markiere zentrale Sammelmodule und prüfe in einem Code-Review, ob eine Änderung lokal bleibt oder unnötig viele Bereiche berührt.