Skalierbare Paketstruktur
Eine durchdachte Paketstruktur macht Code wartbar und Teams produktiv. Du lernst, wie du Features und Schichten sinnvoll trennst.
Wenn ein Android-Projekt wächst, wächst sein Verzeichnisbaum mit – und ohne eine bewusste Strategie landet man schnell in einem Ordner voller Klassen, die nichts miteinander zu tun haben, außer dass sie alle auf denselben Buchstaben enden. Eine skalierbare Paketstruktur löst dieses Problem grundlegend: Sie gibt jeder Klasse einen eindeutigen Heimatort, der sofort verrät, wozu sie gehört, und hält Abhängigkeiten zwischen Verantwortlichkeiten sauber.
Was ist das?
Eine Paketstruktur beschreibt, wie du deine Kotlin-Klassen und Ressourcen in Ordner (Pakete) gruppierst. „Skalierbar” bedeutet, dass die Struktur noch funktioniert, wenn aus zehn Klassen hundert werden und aus einem Entwickler ein Team von fünf oder mehr Personen.
Es gibt zwei grundlegende Denkweisen:
Package by Type gruppiert nach technischer Art: activities, fragments, viewmodels, repositories, models. Anfangs wirkt das ordentlich – bis das Projekt wächst und du für eine einzige User-Story in acht verschiedenen Ordnern arbeitest.
Package by Feature gruppiert nach fachlicher Zugehörigkeit: login, profile, feed, settings. Alles, was zum Login gehört – Screen, ViewModel, Use Case, Repository – lebt im selben Paket. Ein Blick auf die Ordnerstruktur genügt, um zu verstehen, was die App kann.
Die offiziellen Android-Architekturempfehlungen sprechen sich klar für Package by Feature aus, kombiniert mit einer internen Schichtentrennung innerhalb jedes Feature-Pakets.
Wie funktioniert es?
Das bewährte Muster kombiniert zwei Achsen: die Feature-Achse (horizontal) und die Schichten-Achse (vertikal).
Feature-Pakete
Auf oberster Ebene spiegeln die Pakete die Domäne der App wider:
com.example.myapp/
├── login/
├── profile/
├── feed/
└── core/
Das core-Paket nimmt gemeinsam genutzte Utilities, Netzwerk-Clients und Datenbank-Instanzen auf – Dinge, die wirklich app-weit gelten und keinem einzelnen Feature gehören.
Schichten innerhalb eines Features
Innerhalb jedes Feature-Pakets gelten die Schichten der Clean Architecture:
login/
├── ui/
│ ├── LoginScreen.kt // Compose-Funktion
│ └── LoginViewModel.kt
├── domain/
│ ├── LoginUseCase.kt
│ └── UserCredentials.kt // Domain-Modell
└── data/
├── LoginRepository.kt
└── LoginApiService.kt
Die Abhängigkeiten fließen immer von außen nach innen: ui → domain ← data. Die Domäneschicht kennt weder UI noch konkrete Data-Implementierungen – das ist die Grundbedingung für Testbarkeit.
Sichtbarkeit und Kapselung
Kotlin erlaubt internal-Sichtbarkeit auf Modulebene. Klassen, die nur innerhalb eines Features gebraucht werden, bekommen internal – so verhinderst du, dass andere Features direkt auf Implementierungsdetails zugreifen und erzwingst saubere Schnittstellen zwischen den Paketen.
In der Praxis
Ein neues Projekt startet oft mit einer flachen Struktur und einer Handvoll Klassen. Sobald das zweite Feature entsteht, lohnt es sich, die Feature-Aufteilung einzuführen.
Vorher – Package by Type:
// com.example.myapp
// LoginActivity.kt, ProfileActivity.kt,
// UserViewModel.kt, FeedViewModel.kt,
// ApiService.kt, UserRepository.kt ...
Nachher – Package by Feature:
com.example.myapp/
├── login/
│ ├── ui/LoginScreen.kt
│ ├── ui/LoginViewModel.kt
│ └── data/LoginRepository.kt
├── profile/
│ ├── ui/ProfileScreen.kt
│ ├── ui/ProfileViewModel.kt
│ └── data/ProfileRepository.kt
└── core/
└── network/ApiClient.kt
Häufige Stolperfalle: Zirkuläre Abhängigkeiten
Das größte Warnsignal ist, wenn login etwas aus profile importiert und profile gleichzeitig etwas aus login. Das zeigt, dass gemeinsam genutzte Konzepte in core verschoben werden sollten, statt Features voneinander abhängig zu machen.
Ein praktischer Selbsttest: Kannst du ein Feature-Paket gedanklich löschen, ohne dass ein anderes Feature-Paket einen Compile-Fehler bekäme? Wenn nein, sind die Grenzen nicht sauber gezogen.
Der Weg zur Modularisierung
Eine saubere Package-by-Feature-Struktur ist die Vorstufe zu echten Gradle-Modulen. Wenn du später Feature-Module einführst, musst du deine Klassen kaum verschieben – die Pakete werden schlicht zu Modulen. Wer von Anfang an Package by Type verwendet hat, steht vor einer aufwendigen Umstrukturierung, bevor die eigentliche Arbeit beginnen kann.
Fazit
Eine skalierbare Paketstruktur ist keine kosmetische Entscheidung, sondern eine architektonische Weichenstellung. Sie legt fest, wie schnell Entwickler Verantwortlichkeiten finden, wie leicht Tests geschrieben werden können und wie reibungslos das Projekt später in Module aufgeteilt werden kann. Schau dir jetzt die Paketstruktur deines aktuellen Projekts an: Kannst du einem neuen Teammitglied erklären, warum jede Klasse dort liegt, wo sie liegt? Kannst du ein Feature isoliert betrachten, ohne zehn andere Ordner öffnen zu müssen? Wenn nicht, ist ein ruhiger Refactoring-Nachmittag die beste Investition, bevor das nächste Feature hinzukommt.