Android Coden
Android 7 min lesen

Funktionale Prinzipien in Android

Funktionale Prinzipien helfen dir, State-Änderungen klar zu modellieren. So werden Android-Code, Tests und Reviews leichter nachvollziehbar.

Funktionale Prinzipien sind keine eigene Android-Bibliothek, sondern Denk- und Schreibgewohnheiten für verlässlichen Code. Wenn du State-Änderungen als klare Transformationen formulierst, kannst du sie besser testen, in Compose sauber darstellen und in Reviews schneller begründen. Besonders wichtig sind dabei reine Funktionen, unveränderliche Daten und Komposition.

Was ist das?

Funktionale Prinzipien bedeuten im Android-Alltag: Du trennst Berechnung von Wirkung. Eine Berechnung nimmt Eingaben entgegen und liefert ein Ergebnis. Eine Wirkung verändert etwas außerhalb der Funktion, zum Beispiel eine Datenbank, ein Log, eine Netzwerkverbindung oder den UI-State. Beides ist in Apps nötig, aber du solltest sie nicht unnötig vermischen.

Eine reine Funktion ist eine Funktion, deren Ergebnis nur von den Parametern abhängt. Sie liest keinen versteckten globalen Zustand, schreibt nichts nach außen und liefert bei gleichen Eingaben immer dasselbe Ergebnis. Das macht sie ideal für Tests. Du brauchst kein Android-Gerät, keinen Emulator und keine komplizierte Testumgebung, um zu prüfen, ob eine Preisberechnung, ein Filter oder eine State-Transformation korrekt ist.

Immutability bedeutet, dass du Daten nicht direkt veränderst, sondern neue Werte erzeugst. In Kotlin passt das gut zu val, data class und copy. Statt ein Objekt an mehreren Stellen zu verändern, beschreibst du, wie aus altem Zustand neuer Zustand wird. Das ist besonders in Jetpack Compose hilfreich, weil Compose UI aus State ableitet. Wenn sich der State klar und nachvollziehbar ändert, kann auch die Oberfläche nachvollziehbar reagieren.

Komposition bedeutet, dass du größere Abläufe aus kleinen Bausteinen zusammensetzt. Eine Funktion validiert Eingaben, eine andere formatiert Text, eine dritte erzeugt ein UI-Modell. Jede Funktion bleibt überschaubar. Zusammen bilden sie das Verhalten, das du brauchst. Für Lernende ist das ein wichtiger Schritt: Du schreibst nicht mehr eine lange Funktion, die alles erledigt, sondern mehrere kleine Einheiten mit klarer Verantwortung.

Wie funktioniert es?

Das mentale Modell ist einfach: Daten fließen durch deine App. Eingaben kommen aus UI, Repository, Datenbank oder Netzwerk. Dein Code transformiert diese Eingaben in einen neuen Zustand oder in ein Modell, das die UI anzeigen kann. Je mehr dieser Transformationen reine Funktionen sind, desto kleiner wird der Bereich, in dem Fehler schwer zu finden sind.

In einer modernen Android-Architektur liegt dieser Gedanke oft im ViewModel. Das ViewModel hält UI-State, verarbeitet Aktionen aus der Oberfläche und ruft bei Bedarf Repositories auf. Funktionale Prinzipien bedeuten hier nicht, dass das ViewModel keine Seiteneffekte haben darf. Netzwerkaufrufe, Datenbankzugriffe und Navigation existieren weiterhin. Entscheidend ist, dass du die eigentliche Logik möglichst aus diesen Seiteneffekten herausziehst.

Ein typisches Beispiel ist ein Screen mit Suchfeld, Filter und Ladezustand. Die UI sendet eine Aktion wie QueryChanged. Das ViewModel aktualisiert den State. Die reine Logik kann in einer Funktion liegen, die aus alter Query, neuer Query und vorhandenen Daten ein neues UI-Modell baut. Diese Funktion kannst du ohne Compose und ohne Android-Framework testen.

Compose verstärkt diesen Stil, weil Composables idealerweise beschreiben, wie UI für einen bestimmten State aussieht. Ein Composable sollte nicht nebenbei Geschäftslogik verstecken. Wenn du in einer @Composable-Funktion Berechnungen, Datenzugriff und UI vermischst, wird sie schwer testbar und schwer zu lesen. Besser ist: Das ViewModel oder eine eigene Mapper-Funktion bereitet den State vor, Compose zeigt ihn an und sendet Nutzeraktionen zurück.

Unveränderlichkeit spielt dabei eine zentrale Rolle. Wenn du eine mutable Liste im State hältst und einzelne Elemente direkt änderst, kann das zu schwer sichtbaren Fehlern führen. Compose erkennt Änderungen über State-Objekte und deren Aktualisierung. Wenn du stattdessen eine neue Liste erzeugst und den gesamten State mit copy ersetzt, ist klar: Es gab eine neue Version des Zustands. Diese Klarheit hilft auch beim Debuggen, weil du alte und neue Werte vergleichen kannst.

Tests profitieren direkt davon. Reine Funktionen brauchen keine Mocks, keine Instrumentation und keine Wartezeiten. Du gibst Eingaben hinein und prüfst das Ergebnis. In einer Continuous-Integration-Umgebung laufen solche Unit-Tests schnell und stabil. Das unterstützt Qualitätsziele, weil du Logik früh prüfst, bevor sie in UI-Tests oder manuelle Tests rutscht.

In der Praxis

Stell dir einen Warenkorb-Screen vor. Du möchtest aus Warenkorbpositionen einen UI-State erzeugen. Eine ungünstige Lösung wäre, in der Composable-Funktion über alle Produkte zu iterieren, Preise zu berechnen, Rabatte zu prüfen und nebenbei einen globalen Wert zu verändern. Das funktioniert vielleicht anfangs, wird aber schwer wartbar.

Besser ist eine reine Funktion, die einen vorhandenen State oder eine Liste von Eingaben in ein neues Modell überführt:

data class CartItem(
    val name: String,
    val quantity: Int,
    val priceInCents: Int
)

data class CartUiState(
    val items: List<CartItem>,
    val subtotalInCents: Int,
    val itemCount: Int,
    val canCheckout: Boolean
)

fun buildCartState(items: List<CartItem>): CartUiState {
    val validItems = items.filter { it.quantity > 0 }
    val subtotal = validItems.sumOf { it.quantity * it.priceInCents }

    return CartUiState(
        items = validItems,
        subtotalInCents = subtotal,
        itemCount = validItems.sumOf { it.quantity },
        canCheckout = validItems.isNotEmpty() && subtotal > 0
    )
}

Diese Funktion ist gut testbar. Du kannst prüfen, ob Positionen mit Menge 0 entfernt werden, ob die Summe stimmt und ob canCheckout korrekt gesetzt wird. Du brauchst dafür kein Android-Framework. Ein einfacher Unit-Test reicht:

@Test
fun cartState_marksCheckoutAsAvailable_whenCartHasValidItems() {
    val items = listOf(
        CartItem(name = "Kabel", quantity = 2, priceInCents = 499),
        CartItem(name = "Adapter", quantity = 0, priceInCents = 999)
    )

    val state = buildCartState(items)

    assertEquals(998, state.subtotalInCents)
    assertEquals(2, state.itemCount)
    assertTrue(state.canCheckout)
}

Im ViewModel würdest du diese Funktion nutzen, statt die Berechnung direkt zwischen State-Updates zu verstecken:

class CartViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(buildCartState(emptyList()))
    val uiState: StateFlow<CartUiState> = _uiState.asStateFlow()

    fun updateItems(items: List<CartItem>) {
        _uiState.value = buildCartState(items)
    }
}

Das Beispiel zeigt drei funktionale Gewohnheiten. Erstens: Die Berechnung ist rein. Zweitens: Der State ist eine unveränderliche data class. Drittens: Das ViewModel komponiert Verhalten, indem es StateFlow, eine Transformationsfunktion und UI-State zusammenführt.

Eine praktische Entscheidungsregel lautet: Wenn eine Funktion nur Werte berechnet, sollte sie keine Android-Abhängigkeit kennen. Sie sollte kein Context, keine View, keine Datenbankinstanz und keine Coroutine-Scope benötigen. Wenn sie solche Dinge braucht, prüfe, ob du Berechnung und Wirkung trennen kannst. Häufig entsteht dadurch automatisch besserer Code.

Eine typische Stolperfalle ist verdeckte Mutabilität. val items: MutableList<CartItem> sieht auf den ersten Blick stabil aus, weil items nicht neu zugewiesen werden kann. Die Liste selbst kann aber weiterhin verändert werden. Für UI-State ist das oft ungünstig. Verwende lieber List<CartItem> und erzeuge bei Änderungen eine neue Liste. Das ist nicht nur für Compose sauberer, sondern macht auch Tests klarer: Der alte Zustand bleibt alt, der neue Zustand ist ein neuer Wert.

Eine zweite Stolperfalle ist übertriebene Abstraktion. Funktionale Prinzipien bedeuten nicht, dass jede kleine Zeile in eine eigene Funktion ausgelagert werden muss. Wenn eine Funktion durch Aufteilung schwerer zu lesen wird, hast du nichts gewonnen. Ziel ist Verständlichkeit: klare Eingaben, klares Ergebnis, möglichst wenig versteckte Wirkung.

In Code-Reviews kannst du funktionale Prinzipien mit wenigen Fragen prüfen: Hängt das Ergebnis dieser Funktion nur von ihren Parametern ab? Wird State direkt verändert oder als neuer Wert erzeugt? Kann ich die Logik ohne Android-Framework testen? Sind Seiteneffekte bewusst an den Rand gelegt, zum Beispiel ins Repository oder ViewModel? Diese Fragen helfen dir, Qualität nicht nur am Gefühl festzumachen.

Auch beim Debuggen ist der Nutzen konkret. Wenn ein Screen falsche Daten zeigt, kannst du die State-Transformation isoliert prüfen. Du setzt einen Breakpoint vor und nach buildCartState, vergleichst Eingaben und Ausgabe und weißt schnell, ob der Fehler in der Berechnung, im Datenfluss oder in der UI liegt. Genau diese Trennung macht größere Apps beherrschbarer.

Fazit

Funktionale Prinzipien helfen dir, Android-Code in berechenbare Teile zu zerlegen: reine Funktionen für Logik, unveränderliche Daten für nachvollziehbaren State und Komposition für verständliche Abläufe. Übe das an einem bestehenden Screen: Suche eine Berechnung in ViewModel oder Composable, ziehe sie in eine reine Funktion, schreibe zwei Unit-Tests und prüfe im Review, ob State-Änderungen als neue Werte sichtbar werden. So entwickelst du eine Gewohnheit, die bei Compose, Tests und langfristiger Codequalität direkt spürbar wird.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

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