Klassen in Kotlin für Android
Klassen modellieren App-Konzepte mit Zustand und Verhalten. Du lernst Konstruktoren, Properties und typische Android-Fallen kennen.
Klassen gehören zu den Grundbausteinen, mit denen du in Kotlin echte App-Ideen ausdrückst: ein Benutzerprofil, eine Aufgabe, ein Warenkorb, ein Repository oder ein UI-State. Statt lose Variablen und Funktionen nebeneinander zu legen, gibst du einem Konzept einen Namen, definierst seine Daten und beschreibst, welches Verhalten zu diesen Daten gehört.
Was ist das?
Eine Klasse ist ein Bauplan für Objekte. Der Bauplan sagt: Welche Properties hat dieses Objekt, wie wird es erzeugt, und welche Funktionen gehören fachlich dazu? In Android nutzt du Klassen ständig, auch wenn du sie nicht immer bewusst als eigenes Thema wahrnimmst. Eine Activity ist eine Klasse, ein ViewModel ist eine Klasse, ein Datenmodell aus deiner API ist meist eine Klasse, und auch ein Zustand für einen Compose-Screen wird häufig als Klasse oder data class modelliert.
Das mentale Modell ist wichtig: Eine Klasse ist nicht nur eine Schachtel für Daten. Sie beschreibt ein Konzept deiner App. Wenn du eine Notiz-App baust, ist Note nicht bloß eine Sammlung aus title, text und createdAt. Sie steht für die fachliche Idee einer Notiz. Diese Idee kann Regeln haben: Ein Titel darf nicht leer sein, eine archivierte Notiz soll anders angezeigt werden, eine Notiz kann als Favorit markiert werden. Solche Regeln gehören oft näher an das Modell als an zufällige UI-Funktionen.
In Kotlin beginnt eine Klasse mit dem Schlüsselwort class. Häufig schreibst du den primären Konstruktor direkt in die Kopfzeile. Dort definierst du, welche Werte beim Erzeugen eines Objekts übergeben werden. Properties sind benannte Werte innerhalb der Klasse. Mit val sind sie nach der Initialisierung nicht neu zuweisbar, mit var schon. Diese Entscheidung ist mehr als Syntax: Sie beeinflusst, wie vorhersehbar dein Code bleibt.
Für Android ist das besonders relevant, weil Apps aus vielen Schichten bestehen. UI, ViewModel, Datenquelle und Netzwerkmodell arbeiten zusammen. Gute Klassen helfen dir, diese Schichten sauber zu halten. Sie machen sichtbar, was ein Objekt braucht und was es leisten soll. Schlechte Klassen verstecken dagegen Abhängigkeiten, erlauben zu viele Änderungen von außen oder vermischen UI-Details mit Fachlogik.
Wie funktioniert es?
Eine Kotlin-Klasse kann einen primären Konstruktor, Properties, Funktionen und Initialisierungslogik enthalten. Der Konstruktor nimmt Werte entgegen, die ein Objekt für einen gültigen Startzustand braucht. Properties speichern diesen Zustand oder leiten Werte daraus ab. Funktionen beschreiben Verhalten, das sinnvoll zu diesem Objekt gehört.
Ein einfacher Klassenkopf kann so gelesen werden: Der Klassenname sagt, welches Konzept modelliert wird. Die Parameter im Konstruktor sagen, welche Informationen notwendig sind. val und var sagen, ob diese Informationen nachträglich geändert werden dürfen. Sichtbarkeiten wie private steuern, wer auf Details zugreifen darf.
In Kotlin gibt es zusätzlich data class. Sie ist besonders nützlich für Datenmodelle und UI-State, weil Kotlin dir dafür Funktionen wie copy, equals und toString erzeugt. Das ist im Android-Alltag sehr praktisch. Wenn ein Compose-Screen einen Zustand als data class bekommt, kannst du mit copy gezielt einen neuen Zustand erzeugen, statt ein bestehendes Objekt unkontrolliert zu verändern.
Dabei solltest du den Unterschied zwischen Objektzustand und beobachtbarem UI-State verstehen. Compose zeichnet UI auf Basis von State neu. Wenn du eine Property in einem normalen Objekt änderst, weiß Compose davon nicht automatisch. Compose reagiert auf State-Objekte wie MutableState, StateFlow, LiveData mit passender Integration oder andere beobachtbare Quellen. Eine Klasse kann also Zustand enthalten, aber nicht jeder Zustand ist automatisch UI-relevant beobachtbar.
Ein weiteres Konzept ist Kapselung. Du musst nicht jede Property öffentlich veränderbar machen. Oft ist es besser, Werte von außen lesbar zu machen, Änderungen aber über Funktionen zu steuern. So kannst du Regeln erzwingen. Wenn eine Cart-Klasse zum Beispiel die Anzahl von Artikeln verwaltet, sollte nicht jeder Teil deiner App direkt eine negative Anzahl setzen können. Eine Funktion wie addItem oder removeItem kann prüfen, ob die Änderung gültig ist.
Im Android-Code erscheinen Klassen in mehreren Rollen. Datenklassen modellieren Antworten aus einer Datenbank oder API. ViewModel-Klassen koordinieren UI-Zustand und fachliche Aktionen. Repository-Klassen bündeln Datenzugriffe. UI-State-Klassen beschreiben, was ein Screen anzeigen soll. Diese Rollen sind nicht austauschbar. Eine Klasse wird besser verständlich, wenn sie eine klare Verantwortung hat und nicht alles gleichzeitig erledigt.
Wichtig ist auch der Konstruktor als Qualitätsgrenze. Wenn eine Klasse ohne bestimmte Werte keinen Sinn ergibt, gehören diese Werte in den Konstruktor. Wenn eine Klasse eine Abhängigkeit braucht, etwa ein Repository im ViewModel, sollte auch diese Abhängigkeit sichtbar übergeben werden. Versteckte Initialisierung erschwert Tests, Debugging und Code-Review.
In der Praxis
Stell dir einen Compose-Screen vor, der eine Profilkarte anzeigt. Du brauchst ein Modell für die Daten und einen UI-State, der beschreibt, ob gerade geladen wird, ob ein Fehler vorliegt und welches Profil angezeigt werden kann. Eine Klasse hilft dir, diese Informationen geordnet zu halten.
data class UserProfile(
val id: String,
val displayName: String,
val email: String,
val isPremium: Boolean
) {
val initials: String
get() = displayName
.split(" ")
.filter { it.isNotBlank() }
.take(2)
.joinToString("") { it.first().uppercase() }
fun canUsePremiumFeature(): Boolean {
return isPremium
}
}
data class ProfileUiState(
val isLoading: Boolean = false,
val profile: UserProfile? = null,
val errorMessage: String? = null
)
class ProfileViewModel(
private val repository: ProfileRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUiState(isLoading = true))
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
fun loadProfile(userId: String) {
viewModelScope.launch {
_uiState.value = ProfileUiState(isLoading = true)
runCatching {
repository.getProfile(userId)
}.onSuccess { profile ->
_uiState.value = ProfileUiState(profile = profile)
}.onFailure {
_uiState.value = ProfileUiState(
errorMessage = "Profil konnte nicht geladen werden."
)
}
}
}
}
In diesem Beispiel siehst du mehrere Entscheidungen. UserProfile ist eine data class, weil sie hauptsächlich Daten trägt. Die Properties sind val, weil ein geladenes Profil nicht nebenbei verändert werden soll. Wenn sich ein Profil ändert, erzeugst du ein neues Objekt. Die abgeleitete Property initials speichert keinen zusätzlichen Zustand, sondern berechnet ihren Wert aus displayName. Das hält das Modell schlank.
ProfileUiState ist ebenfalls eine data class, weil UI-Zustand oft als unveränderbarer Schnappschuss gedacht wird. Das ViewModel veröffentlicht einen StateFlow, aber hält den veränderbaren _uiState privat. Diese Trennung ist eine typische Android-Regel: Von außen darf gelesen werden, geändert wird nur kontrolliert im Besitzer der Klasse. So verhinderst du, dass irgendein Composable oder fremder Service deinen Screen-Zustand direkt überschreibt.
Eine typische Stolperfalle ist eine zu mutable Klasse. Anfänger schreiben schnell überall var, weil es bequem wirkt. Dadurch kann aber jede Stelle im Code den Zustand ändern. Fehler werden dann schwerer zu finden, weil du nicht mehr weißt, wann ein Wert angepasst wurde. Nutze als Grundregel val, bis du einen echten Grund für var hast. Wenn eine Änderung fachlich wichtig ist, gib ihr eine Funktion mit einem klaren Namen.
Eine zweite Stolperfalle betrifft Compose. Wenn du innerhalb eines Objekts eine normale var änderst, wird die UI nicht zwingend neu gezeichnet. Beispiel: profile.displayName = "Neu" ist nicht nur fachlich fragwürdig, sondern kann auch am State-System vorbeigehen. Besser ist ein neuer UI-State, etwa mit copy, der über StateFlow oder Compose-State veröffentlicht wird. Compose sieht dann eine neue Zustandsversion und kann korrekt reagieren.
Eine praktische Entscheidungsregel lautet: Baue eine Klasse, wenn mehrere Werte gemeinsam ein App-Konzept bilden oder wenn Verhalten eindeutig zu diesen Werten gehört. Baue keine Klasse nur, um zwei beliebige Parameter künstlich zusammenzufassen. Der Name der Klasse sollte in deiner Fachsprache Sinn ergeben. UserProfile, CheckoutState oder LoginAttempt sind verständlich. Namen wie DataHolder, Manager oder Helper sind oft Warnzeichen, weil sie keine klare Verantwortung zeigen.
Beim Testen kannst du Klassen sehr direkt prüfen. Für UserProfile würdest du testen, ob initials bei verschiedenen Namen korrekt berechnet wird. Für ProfileViewModel würdest du ein Fake-Repository übergeben und prüfen, ob nach erfolgreichem Laden ein State mit Profil entsteht. Genau hier zahlt sich ein sauberer Konstruktor aus: Wenn Abhängigkeiten sichtbar übergeben werden, kannst du sie im Test ersetzen.
Auch im Code-Review sind Klassen ein guter Prüfpunkt. Frage dich: Ist der Konstruktor ehrlich über die benötigten Daten? Sind Properties absichtlich val oder var? Hat die Klasse eine klare Verantwortung? Verändert sie Zustand so, dass UI und Tests es nachvollziehen können? Wenn du diese Fragen beantworten kannst, wächst dein Code nicht nur syntaktisch, sondern auch strukturell in Richtung professioneller Android-Entwicklung.
Fazit
Klassen helfen dir, App-Konzepte mit Zustand und Verhalten sauber auszudrücken. Achte besonders auf Konstruktoren, sinnvolle Properties und kontrollierte Veränderbarkeit. Übe das aktiv: Nimm einen kleinen Screen aus deinem Projekt, benenne seine Modelle klar, ersetze unnötige var-Properties durch val, und prüfe im Debugger oder mit Tests, wann neue Objekte entstehen und wann Zustand veröffentlicht wird. So lernst du nicht nur Kotlin-Syntax, sondern entwickelst ein belastbares Gefühl dafür, wie Android-Code lesbar, testbar und wartbar bleibt.