Android Coden
Android 8 min lesen

Design Patterns im Überblick

Design Patterns helfen dir, Architekturentscheidungen klar zu benennen und ihre Kosten zu verstehen.

Design Patterns sind kein Katalog von Lösungen, die du mechanisch in jede Android-App einbaust. Sie sind vor allem ein gemeinsames Vokabular: Du erkennst wiederkehrende Probleme, kannst über mögliche Lösungen sprechen und verstehst die jeweiligen Kompromisse. Genau das ist im Android-Alltag wichtig, weil moderne Apps aus UI, Zustand, Datenquellen, Nebenläufigkeit, Tests und Release-Anforderungen bestehen. Wenn du Patterns richtig einordnest, schreibst du nicht automatisch mehr Code, sondern triffst bewusstere Entscheidungen.

Was ist das?

Ein Design Pattern beschreibt eine bewährte Grundidee für ein wiederkehrendes Entwurfsproblem in Software. Es ist keine fertige Klasse, kein Framework und kein Stück Code, das du kopierst. Ein Pattern sagt eher: „Wenn du dieses Problem hast, kann diese Struktur helfen, aber achte auf diese Kosten.“

Für Android bedeutet das: Du arbeitest ständig mit Entwurfsfragen. Wo liegt Zustand? Wer spricht mit der Datenbank? Wie bleibt eine Compose-UI testbar? Wie vermeidest du, dass eine Activity zu viel Verantwortung bekommt? Wie kannst du Logik austauschen, ohne jede View anzufassen? Patterns geben dir Begriffe für solche Fragen.

Ein einfaches Beispiel ist das Repository-Pattern. Es beschreibt die Idee, Datenzugriffe hinter einer klaren Schnittstelle zu bündeln. Deine UI oder dein ViewModel muss dann nicht wissen, ob Daten aus Room, Retrofit, DataStore oder einem Cache kommen. Das Pattern löst nicht jedes Datenproblem, aber es gibt dir eine Sprache: „Diese Schicht kapselt Datenquellen und stellt dem Rest der App ein stabiles Modell bereit.“

Ein anderes Beispiel ist Observer. In Android begegnet dir diese Idee bei Flow, StateFlow, LiveData oder Compose State. Eine Stelle veröffentlicht Änderungen, andere Stellen reagieren darauf. Du musst nicht immer das Wort „Observer“ verwenden, aber das mentale Modell hilft dir: Zustand ändert sich, Abnehmer werden informiert, und du musst Lebenszyklus, Backpressure oder Wiederholungen beachten.

Der zentrale Punkt: Patterns sind Kommunikationswerkzeuge. Wenn ein Team sagt „wir nutzen hier Dependency Injection“, ist damit nicht nur eine Library wie Hilt gemeint. Gemeint ist die Entkopplung zwischen Objekt-Erstellung und Objekt-Nutzung. Das erleichtert Tests, reduziert harte Abhängigkeiten und macht Architekturentscheidungen sichtbarer.

Wie funktioniert es?

Du solltest ein Pattern in drei Teilen verstehen: Problem, Struktur und Tradeoff. Ohne Problem ist ein Pattern nur Dekoration. Ohne Struktur bleibt es ein vager Begriff. Ohne Tradeoff wirkt es immer richtig, obwohl jedes Pattern Kosten hat.

Das Problem beschreibt die wiederkehrende Spannung. Beim Repository-Pattern ist das zum Beispiel: Mehrere Datenquellen sollen von der App genutzt werden, aber UI-Code soll nicht von technischen Details abhängen. Bei Dependency Injection ist das Problem: Klassen brauchen Abhängigkeiten, sollen sie aber nicht selbst bauen, weil das Tests und Austauschbarkeit erschwert. Bei Model-View-ViewModel geht es darum, UI-Zustand und UI-Logik aus Activity, Fragment oder Composable herauszuhalten.

Die Struktur beschreibt die Rollen. Beim Repository gibt es eine Schnittstelle oder Klasse, die Datenoperationen anbietet. Beim ViewModel gibt es eine langlebigere Komponente, die Zustand für die UI bereitstellt. Bei Observer gibt es eine Quelle und Abnehmer. Diese Rollen sind wichtiger als die konkrete Implementierung. Du kannst dieselbe Idee mit Interfaces, Konstruktorparametern, Coroutines, Flow, Compose State oder Test-Doubles umsetzen.

Der Tradeoff beschreibt den Preis. Ein Repository kann Tests vereinfachen, aber auch unnötige Weiterleitungs-Klassen erzeugen, wenn es nur eine einzelne Funktion ohne echte Abstraktion kapselt. Dependency Injection macht Abhängigkeiten sichtbar, kann aber durch Module, Scopes und generierten Code zusätzliche Komplexität bringen. MVVM kann Verantwortlichkeiten klären, aber auch zu übergroßen ViewModels führen, wenn du jede fachliche Entscheidung dort sammelst.

Im Android-Kontext ist dieser Blick besonders wichtig, weil viele Patterns bereits in Jetpack-APIs angelegt sind. ViewModel, Room DAO, Navigation, WorkManager, Compose State und Flow bringen eigene Entwurfsmodelle mit. Du musst nicht gegen diese Modelle arbeiten. Besser ist es, ihre Rollen zu verstehen und deine App-Struktur daran auszurichten.

Compose verändert außerdem den Blick auf UI-Patterns. Du beschreibst UI als Funktion von Zustand. Dadurch werden manche ältere Muster weniger wichtig, während Zustands-Hoisting, unidirektionaler Datenfluss und klare Event-Callbacks wichtiger werden. Auch das sind Pattern-Ideen: Zustand fließt nach unten, Ereignisse fließen nach oben. Dieses Vokabular hilft dir im Code-Review deutlich mehr als die Frage, ob ein bestimmtes klassisches Pattern „korrekt“ umgesetzt wurde.

Testing ist ein guter Prüfstein für Pattern-Entscheidungen. Wenn eine Klasse schwer zu testen ist, versteckt sie oft Abhängigkeiten, mischt Verantwortlichkeiten oder koppelt sich zu stark an Android-Framework-Code. Die Android-Testgrundlagen betonen, dass du Logik durch passende Testarten absichern solltest. Patterns helfen dabei, weil sie Grenzen im Code schaffen: UI-Test, Unit-Test und Integrationstest können dann gezielter greifen.

Qualität entsteht nicht dadurch, dass ein Pattern im Code auftaucht. Qualität entsteht, wenn die Struktur verständlich bleibt, Fehler früher auffallen und Änderungen kontrollierbar sind. Genau deshalb gehören Patterns zur Software-Engineering-Grundlage: Sie verbinden Lesbarkeit, Wartbarkeit, Testbarkeit und Teamkommunikation.

In der Praxis

Stell dir vor, du baust eine kleine Aufgaben-App mit Compose. Die App zeigt Aufgaben aus einer lokalen Datenbank und synchronisiert später mit einer API. Eine anfängliche, aber problematische Lösung wäre: Das Composable ruft direkt eine Datenbankklasse auf, startet Coroutines selbst und entscheidet auch noch, wie Fehler angezeigt werden. Das funktioniert kurz, wird aber schnell schwer zu testen und schwer zu ändern.

Eine bessere Struktur nutzt mehrere Pattern-Ideen, ohne sie aufzublasen: Das ViewModel hält UI-Zustand, ein Repository kapselt Datenzugriff, und Abhängigkeiten werden von außen übergeben. Die Compose-UI bleibt dadurch nah an ihrer Aufgabe: anzeigen und Events melden.

data class TaskUiState(
    val isLoading: Boolean = false,
    val tasks: List<Task> = emptyList(),
    val errorMessage: String? = null
)

interface TaskRepository {
    fun observeTasks(): Flow<List<Task>>
    suspend fun addTask(title: String)
}

class TaskViewModel(
    private val repository: TaskRepository
) : ViewModel() {

    val uiState: StateFlow<TaskUiState> =
        repository.observeTasks()
            .map { tasks -> TaskUiState(tasks = tasks) }
            .catch { error ->
                emit(TaskUiState(errorMessage = error.message ?: "Unbekannter Fehler"))
            }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = TaskUiState(isLoading = true)
            )

    fun onAddTask(title: String) {
        viewModelScope.launch {
            repository.addTask(title)
        }
    }
}

@Composable
fun TaskScreen(
    viewModel: TaskViewModel,
    modifier: Modifier = Modifier
) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    TaskContent(
        state = state,
        onAddTask = viewModel::onAddTask,
        modifier = modifier
    )
}

In diesem Beispiel siehst du mehrere praktische Entscheidungen. Das Repository ist nicht da, weil „man das so macht“, sondern weil Datenherkunft und UI getrennt werden sollen. Das ViewModel ist nicht nur ein Ablageort für Variablen, sondern übersetzt Datenströme in UI-Zustand. Das Composable kennt keine Datenbank und keine API. Dadurch kannst du TaskContent separat mit Beispielzuständen prüfen, das ViewModel mit einem Fake-Repository testen und das Repository mit Datenquellen-Tests absichern.

Eine wichtige Entscheidungsregel lautet: Nutze ein Pattern, wenn es eine echte Verantwortung trennt oder eine Änderung einfacher macht. Nutze es nicht, nur um den Code erwachsener wirken zu lassen. Wenn eine App nur eine statische Liste aus lokalem Code anzeigt, brauchst du vielleicht noch kein Repository, keine komplexe DI-Konfiguration und keine abstrakte Domain-Schicht. Wenn aber bald API, Cache, Offline-Verhalten oder Tests dazukommen, kann dieselbe Struktur sinnvoll sein.

Eine typische Stolperfalle ist Pattern-Sammeln. Dabei wird jede Idee aus Artikeln oder Konferenzvideos eingebaut: Repository, Use Case, Mapper, Service, Manager, Coordinator, Factory, Provider. Am Ende besteht eine simple Funktion aus zehn Dateien, und niemand im Team kann mehr erklären, welche Grenze wirklich wichtig ist. Das ist kein guter Entwurf. Ein Pattern muss den Code lesbarer machen oder eine konkrete Änderung absichern.

Eine zweite Stolperfalle ist falsches Vokabular. Wenn ein Team alles „Manager“ nennt, wird Verantwortung unscharf. Ein SessionManager, der Tokens speichert, Netzwerkfehler behandelt, Navigation auslöst und Analytics sendet, ist kein klares Pattern, sondern ein Sammelpunkt. Besser ist es, Rollen präzise zu benennen: AuthRepository, TokenStore, LoginViewModel, AnalyticsTracker. Diese Namen helfen im Code-Review, weil du schneller erkennst, ob eine Klasse zu viel weiß.

Beim Testen zeigt sich der Nutzen besonders deutlich. Ein ViewModel mit injiziertem Repository kann mit einem Fake geprüft werden:

class FakeTaskRepository : TaskRepository {
    private val tasks = MutableStateFlow<List<Task>>(emptyList())

    override fun observeTasks(): Flow<List<Task>> = tasks

    override suspend fun addTask(title: String) {
        tasks.value = tasks.value + Task(title = title)
    }
}

Damit kannst du prüfen, ob ein Event den erwarteten Zustand erzeugt, ohne echte Datenbank, ohne Netzwerk und ohne Android-UI. In Continuous Integration sind solche Tests wertvoll, weil sie schnell laufen und Fehler früh melden. Für UI-Verhalten brauchst du trotzdem eigene Tests, aber die Architektur entscheidet, ob du kleine Teile isoliert prüfen kannst oder nur große, langsamere Szenarien testen musst.

In Code-Reviews solltest du bei Patterns drei Fragen stellen. Erstens: Welches Problem löst diese Struktur konkret? Zweitens: Welche Änderung wird dadurch leichter? Drittens: Welche Kosten entstehen durch zusätzliche Dateien, Abstraktionen oder indirekte Aufrufe? Wenn niemand die erste Frage beantworten kann, ist das Pattern wahrscheinlich nicht gut motiviert.

Für Lernende ist außerdem wichtig: Du musst nicht alle Pattern-Namen auswendig kennen, bevor du gute Android-Apps bauen kannst. Baue zuerst ein sauberes mentales Modell auf. Verantwortung trennen. Abhängigkeiten sichtbar machen. Zustand nachvollziehbar führen. Seiteneffekte begrenzen. Tests ermöglichen. Danach bekommen die Pattern-Namen Bedeutung, weil sie reale Erfahrungen ordnen.

Fazit

Design Patterns helfen dir, Android-Code bewusster zu strukturieren und im Team klarer über Architektur zu sprechen. Behandle sie als Vokabular für Probleme, Rollen und Tradeoffs, nicht als Checkliste. Nimm dir eine vorhandene kleine Feature-Strecke in deiner App und markiere, welche Klassen UI, Zustand, Datenzugriff und Nebenwirkungen übernehmen. Schreibe dann einen kurzen Test oder gehe mit dem Debugger durch den Datenfluss. Wenn du erklären kannst, warum eine Grenze existiert, welche Änderung sie erleichtert und welche Kosten sie hat, hast du das Pattern wirklich verstanden.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

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