Android Coden
Android 8 min lesen

Kotlin Properties: Getter, Setter und Backing Fields

Du lernst, wie Kotlin Properties statt direkter Felder arbeiten. Getter, Setter und Backing Fields werden im Android-Alltag greifbar.

Wenn du von Java oder einer anderen Sprache kommst, wirken Kotlin Properties zuerst wie normale Variablen. Du schreibst user.name und liest oder änderst den Wert. Der wichtige Unterschied: In Kotlin greifst du dabei nicht direkt auf ein öffentliches Feld zu. Du arbeitest mit einer Property, die intern über Getter, Setter und bei Bedarf ein Backing Field abgebildet wird. Dieses Modell macht Android-Code kürzer und lesbarer, verlangt aber ein klares Verständnis dafür, wann wirklich Daten gespeichert werden und wann beim Zugriff Logik läuft.

Was ist das?

Eine Property ist in Kotlin ein benannter Zugriffspunkt auf einen Wert. Du deklarierst sie mit val oder var. Eine val-Property ist von außen nur lesbar, eine var-Property ist lesbar und schreibbar. Das sieht ähnlich aus wie ein Feld, ist aber konzeptionell näher an einer Kombination aus Getter und Setter.

Bei val title: String erzeugt Kotlin einen Getter. Bei var title: String erzeugt Kotlin einen Getter und einen Setter. Du musst diese Funktionen meistens nicht selbst schreiben. Der Compiler erstellt sie aus deiner Property-Deklaration. Genau deshalb fühlt sich Kotlin im Android-Alltag kompakter an als klassischer Java-Code mit vielen getName()- und setName()-Methoden.

Das mentale Modell sollte so aussehen: Eine Property beschreibt, wie ein Wert gelesen oder geschrieben wird. Ob dahinter ein gespeicherter Wert liegt, ist eine Implementierungsfrage. Manchmal gibt es ein Backing Field, also ein internes Feld, in dem Kotlin den Wert speichert. Manchmal wird der Wert bei jedem Zugriff berechnet. Beides sieht an der Aufrufstelle gleich aus.

Im Android-Kontext begegnen dir Properties ständig: in Data Classes für API-Modelle, in ViewModels für UI-State, in Compose-State-Objekten, in Konfigurationswerten, in Repository-Klassen und bei Dependency Injection. Du liest state.isLoading, user.displayName oder viewModel.uiState, ohne dir an der Aufrufstelle Gedanken über Getter-Methoden zu machen. Für sauberen Code ist das ein Vorteil. Für Debugging und Architektur ist es aber wichtig zu wissen, dass hinter dieser Oberfläche mehr passieren kann als ein direkter Speicherzugriff.

Kotlin Properties lösen ein wiederkehrendes Problem: Du willst Daten verständlich ausdrücken, aber nicht überall Boilerplate-Code schreiben. Gleichzeitig willst du Zugriff kontrollieren können. Ein Setter kann Werte prüfen, ein Getter kann einen abgeleiteten Wert liefern, und ein privater Setter kann verhindern, dass andere Klassen deinen Zustand unkontrolliert verändern.

Wie funktioniert es?

Die einfachste Property speichert einen Wert:

class Profile(
    val id: String,
    var name: String
)

id ist nur lesbar, name ist lesbar und schreibbar. Kotlin erzeugt dafür die passenden Zugriffsfunktionen. Von außen schreibst du trotzdem profile.name, nicht profile.getName(). Auf JVM-Ebene werden daraus Methoden, die gut mit Java und Android-Frameworks zusammenarbeiten.

Du kannst Getter und Setter selbst definieren:

class BatteryInfo(level: Int) {
    var level: Int = level
        set(value) {
            field = value.coerceIn(0, 100)
        }

    val isLow: Boolean
        get() = level < 20
}

Hier siehst du zwei zentrale Konzepte. Im Setter von level wird field verwendet. Dieses spezielle Wort ist nur innerhalb des Getters oder Setters verfügbar. Es bezeichnet das Backing Field der Property. Ohne field würdest du im Setter versehentlich wieder die Property selbst setzen. Das kann zu einer Endlosschleife führen.

isLow hat dagegen kein eigenes Backing Field. Der Wert wird beim Lesen aus level berechnet. Das ist sinnvoll, weil isLow keine unabhängige Information ist. Wenn du level änderst, bleibt isLow automatisch korrekt.

Ein Backing Field entsteht also nicht immer. Kotlin erzeugt es, wenn eine Property einen gespeicherten Wert braucht. Bei einer Property mit Initialwert und Standard-Getter ist das der Fall. Bei einer rein berechneten Property mit get() = ... braucht Kotlin kein Feld. Für Anfänger ist diese Unterscheidung wichtig, weil sie erklärt, warum manche Properties Zustand besitzen und andere nur eine Sicht auf vorhandenen Zustand sind.

Getter und Setter sollten klein und vorhersehbar bleiben. Technisch kannst du dort fast jede Logik ausführen: Logging, Formatierung, Validierung, Zugriff auf andere Objekte oder sogar Datenbankoperationen. In gutem Android-Code solltest du das stark begrenzen. Ein Property-Zugriff sieht harmlos aus. Wenn dahinter Netzwerkaufrufe, lange Berechnungen oder Seiteneffekte stecken, wird der Code schwer zu verstehen und schwer zu testen.

Auch die Sichtbarkeit ist wichtig. Du kannst eine Property öffentlich lesbar, aber nur intern schreibbar machen:

class LoginViewModel {
    var email: String = ""
        private set

    fun updateEmail(input: String) {
        email = input.trim()
    }
}

Von außen kann email gelesen werden. Änderungen laufen aber über updateEmail. Das ist im ViewModel-Kontext nützlich, weil du Eingaben prüfen, normalisieren oder mit weiteren Zustandsänderungen verbinden kannst. In echten Jetpack-Architekturen würdest du UI-State oft über StateFlow, LiveData oder Compose-State veröffentlichen. Das Grundprinzip bleibt gleich: Die Property ist die lesbare Schnittstelle, die Kontrolle über Änderungen bleibt in der Klasse.

In Compose kommt noch ein weiterer Punkt dazu. Dort siehst du häufig Properties mit Delegation, etwa var text by remember { mutableStateOf("") }. Das ist ein verwandtes Kotlin-Konzept, aber nicht dasselbe wie ein simples Backing Field. Die Property leitet Lesen und Schreiben an ein Delegate-Objekt weiter. Für diesen Artikel reicht die Abgrenzung: Auch dort greifst du über Property-Syntax zu, aber die Speicherung und Benachrichtigung passieren in einem Objekt hinter der Property. Gerade bei Compose ist es deshalb wichtig, nicht anzunehmen, dass text = "Hallo" nur ein lokales Feld überschreibt. Es kann eine Recomposition auslösen.

In der Praxis

Stell dir vor, du baust ein Profilformular in einer Android-App. Du willst einen Benutzernamen speichern, aber führende und folgende Leerzeichen entfernen. Außerdem willst du für die UI einen Anzeigenamen liefern, der nie leer ist.

class EditableProfile(
    username: String
) {
    var username: String = username
        set(value) {
            field = value.trim()
        }

    val displayName: String
        get() = username.ifBlank { "Unbekannter Nutzer" }
}

Wenn du profile.username = " Lea " setzt, speichert der Setter "Lea". Wenn du profile.displayName liest, wird der Wert aus username abgeleitet. Das ist ein gutes Beispiel für eine kleine, lokale Regel. Der Setter normalisiert einen Wert. Der Getter berechnet einen UI-freundlichen Text. Beides ist schnell, nachvollziehbar und gut testbar.

Ein passender Unit-Test könnte so aussehen:

import kotlin.test.Test
import kotlin.test.assertEquals

class EditableProfileTest {
    @Test
    fun trimsUsernameWhenAssigned() {
        val profile = EditableProfile("Sam")

        profile.username = "  Lea  "

        assertEquals("Lea", profile.username)
    }

    @Test
    fun usesFallbackDisplayNameWhenUsernameIsBlank() {
        val profile = EditableProfile("   ")

        assertEquals("Unbekannter Nutzer", profile.displayName)
    }
}

Dieser Test prüft nicht nur den aktuellen Wert, sondern dein Verständnis des Property-Verhaltens. Du erkennst, dass Schreiben über den Setter läuft und Lesen von displayName eine Berechnung ausführt.

Eine typische Stolperfalle ist der falsche Zugriff im Setter:

class BrokenProfile {
    var username: String = ""
        set(value) {
            username = value.trim()
        }
}

Das sieht plausibel aus, ist aber falsch. Innerhalb des Setters bedeutet username = ... wieder: Rufe den Setter von username auf. Dadurch ruft sich der Setter selbst erneut auf. Richtig ist field = value.trim(), weil field direkt das Backing Field beschreibt.

Eine zweite Stolperfalle sind teure Getter. Dieser Code wäre in einer Android-App problematisch:

val userCount: Int
    get() = database.userDao().countUsers()

Auch wenn die Syntax wie ein harmloser Wert aussieht, kann hier Datenbankarbeit passieren. In UI-Code, besonders in Compose, kann ein Getter häufiger gelesen werden, als du erwartest. Wenn der Getter langsam ist oder Seiteneffekte hat, bekommst du Performance-Probleme und schwer erklärbares Verhalten. Die bessere Regel lautet: Properties dürfen kleine Berechnungen und reine Ableitungen enthalten. Für Arbeit mit I/O, Datenbank, Netzwerk oder komplexer Geschäftslogik verwendest du Funktionen, suspend-Funktionen, Flows oder klar benannte Use Cases.

Eine praktische Entscheidungsregel hilft dir im Alltag: Wenn der Zugriff wie das Lesen eines Werts verstanden werden soll, nutze eine Property. Wenn der Zugriff Arbeit startet, Zeit braucht, fehlschlagen kann oder einen sichtbaren Seiteneffekt hat, nutze eine Funktion. val displayName ist passend. fun loadUserCount() ist ehrlicher als val userCount, wenn dafür eine Datenquelle abgefragt wird.

Auch bei Architektur ist diese Regel wertvoll. In einem ViewModel kann val uiState: StateFlow<ProfileUiState> eine gute öffentliche Property sein, weil sie einen beobachtbaren Zustand beschreibt. Eine Methode wie refreshProfile() ist dagegen eine Aktion, weil sie Arbeit auslöst. Diese Trennung macht deinen Code für andere Entwickler lesbar und reduziert Missverständnisse in Code-Reviews.

Du solltest außerdem darauf achten, Properties nicht als Versteck für zu viel Logik zu verwenden. Wenn ein Setter mehrere andere Properties verändert, Events sendet und Validierung über mehrere Klassen verteilt, wird der Zustand schwer kontrollierbar. Dann ist eine explizite Methode besser, etwa updateForm(input) oder submitProfileChange(). Properties sind stark, wenn sie lokale Regeln ausdrücken. Sie werden gefährlich, wenn sie ganze Workflows tarnen.

Zum Debuggen kannst du Breakpoints direkt in Getter oder Setter setzen. Das ist besonders hilfreich, wenn du nicht verstehst, warum ein Wert verändert wird. Setze einen Breakpoint in den Setter und führe die App aus. Du siehst dann, welche Aufrufstelle die Änderung auslöst. In Tests kannst du gezielt prüfen, ob Normalisierung, Fallbacks und Sichtbarkeit wie erwartet funktionieren. Im Code-Review solltest du bei jeder benutzerdefinierten Property fragen: Ist diese Logik klein genug, um hinter Property-Syntax verständlich zu bleiben?

Fazit

Kotlin Properties sind ein Kernwerkzeug für klaren Android-Code, weil sie Datenzugriff knapp ausdrücken und trotzdem Kontrolle über Lesen und Schreiben erlauben. Der entscheidende Punkt ist die Abgrenzung zu direktem Feldzugriff: user.name kann einen Getter aufrufen, ein Setter kann validieren, und field verweist nur innerhalb einer Zugriffsfunktion auf das Backing Field. Prüfe dein Verständnis aktiv, indem du eine kleine Klasse mit berechneter val, validierender var und privatem Setter schreibst, Breakpoints in Getter und Setter setzt und die Regeln mit Unit-Tests absicherst.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

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