Android Coden
Android 8 min lesen

Composition: Verhalten flexibel kombinieren

Composition kombiniert kleine Objekte zu flexiblem Verhalten. So vermeidest du starre Vererbung in Android-Code.

Composition hilft dir, Android-Code so zu strukturieren, dass Verhalten aus kleinen, klaren Bausteinen entsteht. Statt eine tiefe Vererbungshierarchie zu bauen, kombinierst du Objekte, Funktionen oder Klassen mit eng begrenzter Verantwortung und gibst Arbeit gezielt weiter.

Was ist das?

Composition bedeutet: Eine Klasse bekommt benötigtes Verhalten nicht dadurch, dass sie von einer immer größeren Basisklasse erbt, sondern dadurch, dass sie andere Objekte benutzt. Du setzt also mehrere spezialisierte Teile zusammen. Jedes Teil löst eine überschaubare Aufgabe, zum Beispiel Daten laden, Eingaben prüfen, Logs schreiben oder einen Zustand formatieren.

Das mentale Modell ist einfach genug für den Einstieg, aber wichtig für professionellen Code: Eine Klasse muss nicht alles selbst können. Sie kann mit anderen Klassen zusammenarbeiten. Wenn ein ViewModel Nutzerdaten anzeigen soll, muss es nicht selbst wissen, wie HTTP funktioniert, wie eine Datenbank abgefragt wird und wie Fehlertexte entstehen. Es kann ein Repository, einen Validator und einen Formatter verwenden. Diese Bausteine werden kombiniert, damit das gewünschte Verhalten entsteht.

Der Gegenentwurf ist eine tiefe Vererbungskette. Du könntest eine Basisklasse BaseScreenViewModel bauen, daraus LoadingViewModel, daraus UserViewModel und daraus weitere Varianten ableiten. Das wirkt zuerst bequem, wird aber schnell starr. Jede neue Anforderung landet in einer Oberklasse oder überschreibt Verhalten an einer Stelle, die schwer zu verstehen ist. Änderungen treffen dann oft mehr Screens als geplant. Composition hält die Richtung enger: Du baust kleine Fähigkeiten und setzt sie dort ein, wo du sie brauchst.

Im Android-Kontext passt Composition gut zu modernem Kotlin, Jetpack und sauberer Architektur. Kotlin unterstützt diesen Stil durch Interfaces, Datenklassen, Funktionen als Werte, Konstruktorparameter und Delegation. Jetpack-Komponenten wie ViewModel, Repository-Schichten, Use Cases und Compose-Funktionen profitieren davon, wenn sie nicht über Vererbung gekoppelt sind. Auch Jetpack Compose trägt den Gedanken im Namen: UI entsteht aus zusammengesetzten Funktionen. Trotzdem geht es hier nicht nur um Compose-UI, sondern um ein allgemeines Architekturprinzip für flexiblen Android-Code.

Die drei Schlüsselwörter sind delegation, reuse und flexibility. Delegation heißt: Ein Objekt gibt eine Aufgabe an ein anderes Objekt weiter. Reuse heißt: Ein Baustein kann an mehreren Stellen genutzt werden, ohne kopiert zu werden. Flexibility heißt: Du kannst Bausteine austauschen, testen oder erweitern, ohne den restlichen Code stark zu verändern.

Wie funktioniert es?

Composition beginnt mit einer klaren Frage: Welche Verantwortung hat diese Klasse wirklich? Alles, was nicht zu dieser Verantwortung gehört, ist ein Kandidat für einen eigenen Baustein. Ein Screen-ViewModel soll Zustand vorbereiten und Aktionen koordinieren. Es sollte nicht gleichzeitig Datenspeicherung, Netzwerkdetails, Datumsformatierung und komplexe Validierung enthalten. Diese Aufgaben werden an passende Objekte delegiert.

In Kotlin wird Composition meistens über Konstruktorparameter sichtbar. Eine Klasse bekommt ihre Abhängigkeiten von außen. Dadurch entsteht keine harte Kopplung an konkrete Implementierungen. Wenn du ein Interface verwendest, kann dieselbe Klasse mit einer echten Implementierung in der App und mit einer Testimplementierung im Unit-Test arbeiten.

Ein typisches Muster sieht so aus: Das ViewModel kennt ein UserRepository, aber nicht dessen interne Details. Das Repository kann wiederum eine API, eine lokale Datenquelle oder beide kombinieren. Für das ViewModel zählt nur: Es kann Nutzerdaten anfragen. Diese Begrenzung macht den Code lesbarer und leichter testbar.

Delegation kann auch sprachlich durch Kotlin unterstützt werden. Kotlin kennt Delegated Properties wie by lazy, und es gibt Interface-Delegation mit by. Für Lernende ist aber zuerst wichtiger, den Architekturgedanken zu verstehen: Du gibst Verhalten an ein Objekt weiter, das dafür zuständig ist. Das Schlüsselwort by ist nur eine mögliche Syntax, nicht der Kern des Prinzips.

Composition funktioniert gut, wenn die Bausteine klein und stabil sind. Ein guter Baustein hat einen eindeutigen Namen, eine klare Schnittstelle und wenige Gründe, sich zu ändern. Ein EmailValidator prüft E-Mail-Adressen. Ein PriceFormatter formatiert Preise. Ein SessionRepository verwaltet Sitzungsdaten. Wenn ein Name mit vielen Zuständigkeiten überladen ist, etwa UserHelperManagerUtil, ist das oft ein Hinweis, dass die Grenzen noch unscharf sind.

Wiederverwendung entsteht nicht durch möglichst allgemeine Klassen, sondern durch präzise Bausteine. Eine sehr große Basisklasse wirkt wiederverwendbar, zwingt aber alle Unterklassen, ihr Modell zu übernehmen. Ein kleines Interface ist oft wertvoller, weil es nur eine Fähigkeit beschreibt. So kannst du eine Implementierung austauschen, ohne die aufrufende Klasse neu zu schreiben.

Für Android ist das besonders relevant, weil Apps mit vielen Lebenszyklen, Datenquellen und UI-Zuständen arbeiten. Ein Screen kann neu erstellt werden, ein Netzwerkrequest kann fehlschlagen, ein Flow kann mehrere Werte senden, und Tests sollen ohne echte Geräte oder Server laufen. Composition hilft dir, diese Teile getrennt zu halten. Du kannst die Datenquelle mocken, den Formatter direkt testen und das ViewModel ohne Android-Framework-Details prüfen.

Eine wichtige Grenze: Composition ist kein Freifahrtschein für zu viele winzige Klassen. Wenn jede einzelne Zeile in ein eigenes Objekt ausgelagert wird, wird der Code nicht klarer. Der Nutzen entsteht, wenn ein Baustein eine fachliche oder technische Verantwortung bündelt, die du benennen, testen und austauschen möchtest. Gute Composition reduziert Abhängigkeiten. Schlechte Composition verteilt Logik so stark, dass du ständig zwischen Dateien springen musst.

In der Praxis

Stell dir vor, du baust einen Profil-Screen. Der Screen soll den Namen eines Nutzers anzeigen und zusätzlich einen kurzen Status ableiten. Ein häufiger Anfängerfehler wäre, alles direkt in das ViewModel zu schreiben: Datenquelle ansprechen, Fehler behandeln, Text formatieren und Regeln für den Status berechnen. Das funktioniert für den ersten Screen, aber bei der zweiten Verwendung beginnst du zu kopieren.

Mit Composition zerlegst du die Aufgaben. Das ViewModel koordiniert. Das Repository liefert Daten. Ein Formatter bereitet Text für die UI auf. Ein Status-Regelwerk entscheidet, welcher Status angezeigt wird. Das folgende Beispiel ist bewusst kompakt, zeigt aber den Kern:

data class User(
    val id: String,
    val name: String,
    val isPremium: Boolean
)

interface UserRepository {
    suspend fun loadUser(id: String): User
}

interface UserStatusFormatter {
    fun format(user: User): String
}

class DefaultUserStatusFormatter : UserStatusFormatter {
    override fun format(user: User): String {
        return if (user.isPremium) {
            "${user.name} nutzt Premium"
        } else {
            "${user.name} nutzt den Standardzugang"
        }
    }
}

data class ProfileUiState(
    val title: String = "",
    val status: String = "",
    val isLoading: Boolean = false
)

class ProfileViewModel(
    private val repository: UserRepository,
    private val statusFormatter: UserStatusFormatter
) : ViewModel() {

    private val _state = MutableStateFlow(ProfileUiState())
    val state: StateFlow<ProfileUiState> = _state.asStateFlow()

    fun load(userId: String) {
        viewModelScope.launch {
            _state.value = _state.value.copy(isLoading = true)

            val user = repository.loadUser(userId)

            _state.value = ProfileUiState(
                title = user.name,
                status = statusFormatter.format(user),
                isLoading = false
            )
        }
    }
}

Das ProfileViewModel erbt nur von ViewModel, weil es den Android-Lifecycle-Kontext für viewModelScope braucht. Das fachliche Verhalten kommt nicht aus einer eigenen Basisklasse, sondern aus zusammengesetzten Abhängigkeiten. Der Formatter kann in einem anderen Screen wiederverwendet werden. Im Test kannst du ein Fake-Repository und einen einfachen Formatter einsetzen. Du musst dafür keine echte API starten und keine Activity öffnen.

Eine konkrete Entscheidungsregel hilft dir im Alltag: Wenn du überlegst, eine Basisklasse zu erweitern, frage zuerst, ob ein kleines Interface oder eine Hilfsklasse die Aufgabe klarer lösen würde. Vererbung ist sinnvoll, wenn eine echte Ist-ein-Beziehung besteht und die Oberklasse ein stabiles, enges Verhalten vorgibt. Composition ist meist besser, wenn du Fähigkeiten kombinieren möchtest. Ein Profil-Screen ist kein Formatter und kein Repository. Er verwendet diese Bausteine.

In Compose siehst du denselben Gedanken auf UI-Ebene. Eine große Composable-Funktion, die App-Bar, Ladezustand, Fehleransicht, Liste, leeren Zustand und Dialoge enthält, wird schwer zu prüfen. Du kannst sie in kleinere Composables zerlegen: ProfileHeader, ProfileStatus, LoadingContent, ErrorContent. Diese Funktionen erben nicht voneinander. Sie werden zusammengesetzt. Der Effekt ist ähnlich wie bei Klassen: Jede Funktion hat eine klare Aufgabe, und der Screen bleibt verständlich.

Eine typische Stolperfalle ist die falsche Richtung der Abhängigkeiten. Composition wird schwach, wenn ein kleiner Baustein plötzlich die ganze App kennt. Ein Formatter sollte kein Context, kein Repository und keinen Navigator brauchen, nur um einen Status-Text zu erstellen. Je mehr ein Baustein weiß, desto schlechter lässt er sich wiederverwenden. Achte deshalb auf schmale Schnittstellen. Übergib nur die Daten, die wirklich gebraucht werden.

Eine zweite Stolperfalle ist verfrühte Abstraktion. Nicht jede doppelte Zeile braucht sofort ein Interface. Wenn du nur eine Implementierung hast und noch nicht weißt, welche Variation später kommt, kann eine konkrete Klasse genügen. Composition bedeutet nicht, überall Interfaces zu erzwingen. Sie bedeutet, Zuständigkeiten so zu trennen, dass der Code lesbar bleibt und Änderungen nicht unnötig streuen.

Du kannst dein Verständnis praktisch prüfen, indem du eine bestehende Klasse öffnest und ihre Verantwortlichkeiten markierst. Schreibe neben jede Methode, ob sie UI-Zustand koordiniert, Daten lädt, formatiert, validiert oder navigiert. Wenn eine Klasse viele Kategorien enthält, suche eine Aufgabe aus und lagere sie in einen fokussierten Baustein aus. Danach schreibst du einen kleinen Unit-Test für diesen Baustein. Im Code-Review solltest du besonders auf Namen, Abhängigkeiten und Testbarkeit achten: Kann ein anderer Entwickler in wenigen Sekunden erkennen, welche Rolle jedes Objekt spielt?

Auch beim Debugging macht Composition den Unterschied. Wenn ein Status-Text falsch ist, setzt du den Breakpoint im Formatter. Wenn die Daten falsch sind, prüfst du das Repository. Wenn der Ladezustand nicht stimmt, prüfst du das ViewModel. Bei einer großen Vererbungskette musst du dagegen oft erst herausfinden, welche Oberklasse welches Verhalten verändert. Composition gibt dir klarere Suchräume.

Fazit

Composition ist eine zentrale Denkweise für sauberen Android-Code: Du baust Verhalten aus kleinen, fokussierten Bausteinen und delegierst Aufgaben an Objekte, die dafür zuständig sind. Dadurch entstehen Wiederverwendung und Flexibilität, ohne dass tiefe Vererbungshierarchien deinen Code festlegen. Prüfe das aktiv an einem eigenen Screen: Finde eine Klasse mit zu vielen Verantwortlichkeiten, extrahiere einen klar benannten Baustein, teste ihn isoliert und besprich im Code-Review, ob die neue Grenze wirklich verständlicher ist.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

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