Kotlin-Dateistruktur verstehen
Du lernst, wie Kotlin-Dateien Code über Packages, Imports und Top-Level-Deklarationen ordnen. Der Fokus liegt auf Android-Projekten.
Wenn du von Java zu Kotlin kommst oder Android gerade neu lernst, wirkt die Dateistruktur zuerst ungewohnt: Eine Kotlin-Datei muss nicht wie eine Klasse heißen, und sie darf mehr enthalten als genau eine öffentliche Klasse. Das ist keine Nebensache, sondern eine wichtige Grundlage für sauberen Android-Code. In echten Projekten entscheidest du ständig, welche Funktionen, Konstanten, UI-Komponenten und Hilfstypen zusammengehören. Kotlin gibt dir dafür mehr Freiheit als Java. Diese Freiheit hilft dir aber nur, wenn du Packages, Imports und Top-Level-Deklarationen bewusst einsetzt.
Was ist das?
Die Kotlin File Structure beschreibt, wie eine einzelne Kotlin-Quelldatei aufgebaut ist. Typisch beginnt sie mit einer optionalen package-Zeile, danach folgen import-Anweisungen, und anschließend kommen Deklarationen wie Klassen, Interfaces, Funktionen, Properties, Objekte oder Enums. Der zentrale Punkt ist: Eine Kotlin-Datei organisiert Code nicht über einen zwingenden Klassennamen. Du kannst mehrere Deklarationen in einer Datei haben, und du kannst Funktionen oder Konstanten direkt auf Datei-Ebene definieren.
In Java war das mentale Modell oft sehr streng: Eine öffentliche Klasse gehört in eine Datei mit gleichem Namen. Kotlin ist flexibler. Die Datei UserFormatter.kt kann zum Beispiel eine Top-Level-Funktion formatUserName, eine private Hilfsfunktion und eine kleine Konstante enthalten, ohne dass du dafür eine künstliche Klasse UserFormatter bauen musst. Trotzdem sollte der Dateiname sinnvoll bleiben. Er ist für Menschen da: für Navigation, Code-Review, Suche und Wartung.
Im Android-Kontext begegnet dir diese Struktur überall. Eine Compose-Datei kann mehrere Composable-Funktionen enthalten. Eine Datei im data-Package kann Mapper-Funktionen aufnehmen. Eine Testdatei kann Testklassen und kleine Testdaten-Fabriken enthalten. Eine Datei mit Konstanten kann Build- oder Feature-spezifische Werte sammeln, wenn diese Werte fachlich zusammengehören. Kotlin erlaubt solche Strukturen, aber es verlangt von dir Disziplin bei der Benennung und beim Zuschnitt.
Das Package ist dabei der logische Namensraum. Es sagt, wo ein Symbol im Projekt fachlich hingehört. In Android-Projekten spiegelt das Dateisystem meistens die Package-Struktur wider, etwa com.example.shop.feature.cart. Technisch ist Kotlin nicht darauf reduziert, aber in Android ist diese Übereinstimmung eine sehr wichtige Konvention. Sie hilft Android Studio, Gradle, Reviews und dir selbst, den Code schnell zu verstehen.
Wie funktioniert es?
Eine Kotlin-Datei folgt einer klaren Reihenfolge. Zuerst kommt das Package, dann die Imports, dann der eigentliche Code. Diese Reihenfolge ist nicht nur Stil, sondern Teil der Sprache. Das Package steht, wenn vorhanden, ganz oben. Es definiert, unter welchem Namen die Deklarationen aus dieser Datei von anderen Dateien erreicht werden. Wenn du also eine Funktion formatPrice im Package com.example.shop.core.formatting definierst, wird sie von außen über diesen Namensraum gefunden.
Imports holen Namen aus anderen Packages in die aktuelle Datei. Ohne Import müsstest du viele Typen voll qualifizieren, also zum Beispiel androidx.compose.material3.Text statt nur Text schreiben. Android Studio verwaltet viele Imports automatisch, aber du solltest sie trotzdem lesen können. Sie zeigen dir, welche Frameworks und Projektbereiche eine Datei verwendet. Eine Compose-Datei mit Imports aus androidx.compose.runtime, androidx.compose.ui und deiner eigenen theme-Struktur verrät dir sofort, dass sie UI-Code enthält.
Top-Level-Deklarationen sind der Teil, der viele Lernende überrascht. In Kotlin darfst du Funktionen, Properties und Typen direkt in die Datei schreiben, ohne sie in eine Klasse zu packen. Das ist praktisch, wenn eine Funktion keinen Zustand besitzt und keine Objektidentität braucht. Eine Formatierungsfunktion, eine kleine Mapping-Funktion oder eine interne Konstante sind typische Kandidaten.
Wichtig ist die Sichtbarkeit. Eine Top-Level-Deklaration kann zum Beispiel public, internal oder private sein. Ohne Angabe ist sie öffentlich innerhalb der üblichen Kotlin-Regeln. In Android-Projekten solltest du nicht jede Hilfsfunktion automatisch öffentlich machen. Wenn eine Funktion nur innerhalb derselben Datei gebraucht wird, setze private. Wenn sie nur innerhalb des Moduls sichtbar sein soll, kann internal passen. Damit schützt du deine Architektur vor zufälliger Kopplung.
Ein gutes mentales Modell lautet: Die Datei ist ein kleiner fachlicher Arbeitsbereich. Das Package ordnet diesen Arbeitsbereich in das Projekt ein. Imports zeigen seine Abhängigkeiten. Top-Level-Deklarationen beschreiben die Dinge, die ohne künstliche Hülle zusammen Sinn ergeben. Wenn du dieses Modell verinnerlichst, erkennst du schneller, ob eine Datei sauber geschnitten ist oder zu viele Aufgaben sammelt.
Im Alltag mit Jetpack Compose ist das besonders sichtbar. Du wirst häufig Dateien sehen, in denen eine öffentliche Screen-Funktion oben steht und darunter private Composable-Helfer folgen. Das ist normal und oft lesbar. Eine Datei CartScreen.kt kann also CartRoute, CartScreen, CartItemRow und vielleicht eine private Preview enthalten. Problematisch wird es, wenn dieselbe Datei zusätzlich Netzwerkmodelle, Repository-Code, Mapper, Navigation und UI-State-Klassen enthält. Dann ist nicht Kotlin das Problem, sondern die fehlende fachliche Grenze.
Auch Imports können Hinweise auf solche Grenzen geben. Wenn eine Datei gleichzeitig retrofit2, androidx.compose, room und mehrere Feature-Packages importiert, ist das ein Warnsignal. Es kann berechtigt sein, aber du solltest es prüfen. Saubere Dateistruktur ist kein Selbstzweck. Sie hält Abhängigkeiten sichtbar und macht Änderungen kleiner.
In der Praxis
Stell dir ein kleines Android-Feature für einen Warenkorb vor. Du möchtest Preise anzeigen und hast dafür eine Formatierungsfunktion. In Kotlin brauchst du dafür keine Klasse mit statischen Methoden. Du kannst eine Datei mit klarer Package-Zuordnung und einer Top-Level-Funktion verwenden.
package com.example.shop.core.formatting
import java.text.NumberFormat
import java.util.Locale
fun formatPrice(cents: Int, locale: Locale = Locale.GERMANY): String {
val euros = cents / 100.0
return NumberFormat.getCurrencyInstance(locale).format(euros)
}
private fun isDiscounted(originalCents: Int, currentCents: Int): Boolean {
return currentCents < originalCents
}
Diese Datei könnte PriceFormatting.kt heißen. Der Name beschreibt den Inhalt, obwohl es keine Klasse PriceFormatting geben muss. Die öffentliche Funktion formatPrice ist von anderen Packages nutzbar. Die Funktion isDiscounted ist dagegen private, weil sie in diesem Beispiel nur innerhalb der Datei verwendet werden soll. Genau diese Sichtbarkeitsentscheidung ist in echten Projekten wichtig.
In einer Compose-Datei könntest du die Funktion dann importieren und verwenden:
package com.example.shop.feature.cart
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.example.shop.core.formatting.formatPrice
@Composable
fun CartTotal(totalCents: Int) {
Text(text = formatPrice(totalCents))
}
Daran siehst du die drei Bausteine sehr deutlich. Das Package com.example.shop.feature.cart ordnet die Datei dem Warenkorb-Feature zu. Die Imports zeigen, dass Compose und eine eigene Formatierungsfunktion verwendet werden. Die Top-Level-Funktion CartTotal steht direkt in der Datei, weil Composable-Funktionen in Kotlin üblicherweise genau so definiert werden.
Eine gute Entscheidungsregel lautet: Lege eine Top-Level-Funktion an, wenn sie zustandslos ist, fachlich klar benannt werden kann und nicht nur als Methode eines bestehenden Objekts verständlicher wäre. Wenn die Funktion eng zu einer Klasse gehört und deren Daten oder Invarianten braucht, gehört sie eher in diese Klasse. Wenn sie viele Abhängigkeiten benötigt, ist vielleicht ein eigener Service, Mapper oder Use Case passender. Top-Level bedeutet nicht automatisch Architektur ohne Struktur.
Eine typische Stolperfalle ist die Datei Utils.kt. Am Anfang wirkt sie bequem: Du legst dort kleine Hilfsfunktionen ab und bist schnell fertig. Nach einigen Wochen enthält sie Datumsformatierung, String-Operationen, UI-Helfer, Testlogik und vielleicht noch Netzwerkprüfungen. Dann ist die Datei schwer zu durchsuchen, schwer zu testen und fachlich unklar. Besser sind konkrete Dateinamen wie PriceFormatting.kt, UserNameValidation.kt, CartMappers.kt oder ImageLoadingDefaults.kt. Der Name sollte dir sagen, warum die Deklarationen zusammenstehen.
Eine zweite Stolperfalle sind wilde Imports. Besonders bei gleichnamigen Klassen kann Android Studio mehrere Varianten anbieten. List kann aus Kotlin kommen, UI-Typen können ähnliche Namen haben, und eigene Klassen können mit Framework-Klassen kollidieren. Prüfe bei Auto-Imports, ob wirklich das richtige Package importiert wurde. Ein falscher Import erzeugt manchmal sofort einen Compilerfehler, manchmal aber nur verwirrendes Verhalten oder schwer lesbare Typen. In Code-Reviews lohnt sich deshalb ein kurzer Blick auf die Import-Liste, vor allem wenn eine Datei plötzlich viele neue Abhängigkeiten bekommen hat.
Auch Package-Namen solltest du nicht beliebig ändern. In Android hängen daran viele Referenzen: Quellcode, Tests, Navigation, Dependency Injection, manchmal auch Manifest- oder Generated-Code-Bezüge. Moderne Android-Projekte nutzen Kotlin und Jetpack stark modularisiert. Wenn du Packages sauber nach Funktion und Verantwortlichkeit strukturierst, fällt es später leichter, Features zu verschieben, Module aufzuteilen oder Tests gezielt zu schreiben.
Für Lernende ist eine praktische Übung sinnvoll: Nimm eine bestehende Datei in einem kleinen Projekt und markiere drei Bereiche. Erstens: Welches Package hat sie, und passt es zum fachlichen Inhalt? Zweitens: Welche Imports zeigen externe Abhängigkeiten? Drittens: Welche Top-Level-Deklarationen gibt es, und gehören sie wirklich zusammen? Danach benenne die Datei gedanklich neu. Wenn dir kein präziser Name einfällt, ist die Datei wahrscheinlich zu breit.
Beim Testen kannst du dein Verständnis ebenfalls prüfen. Eine Top-Level-Funktion wie formatPrice lässt sich direkt in einem Unit-Test aufrufen, ohne Android-Framework und ohne Objektinstanz. Das ist ein Vorteil. Wenn eine Top-Level-Funktion aber plötzlich Context, Ressourcen, Datenbankzugriff oder Netzwerk braucht, solltest du kritisch werden. Dann ist sie nicht mehr nur eine kleine fachliche Operation, sondern Teil einer größeren Abhängigkeit. Diese Grenze früh zu erkennen, gehört zu professioneller Kotlin-Arbeit.
In Code-Reviews kannst du eine kurze Checkliste verwenden: Ist der Dateiname fachlich konkret? Entspricht das Package der Projektstruktur? Sind Imports plausibel und nicht breiter als nötig? Sind Hilfsfunktionen private, wenn sie nur lokal gebraucht werden? Gibt es Top-Level-Deklarationen, die besser in eine Klasse, ein Objekt oder eine eigene Datei gehören? Diese Fragen dauern nicht lange, verhindern aber schleichende Unordnung.
Fazit
Die Kotlin-Dateistruktur gibt dir mehr Freiheit als viele klassische Java-Strukturen: Dateien müssen nicht nach einer einzigen Klasse organisiert sein, und Top-Level-Deklarationen sind ein normales Werkzeug. Nutze diese Freiheit gezielt. Packages geben deinem Code einen logischen Ort, Imports machen Abhängigkeiten sichtbar, und Top-Level-Funktionen eignen sich für kleine, klare, zustandslose Bausteine. Prüfe das Gelernte aktiv an einem echten Android-Projekt: Öffne eine Kotlin-Datei, lies Package und Imports bewusst, bewerte die Top-Level-Deklarationen und schlage in einem Code-Review eine konkretere Aufteilung vor, wenn eine Datei zu viele Aufgaben sammelt.