Observer Pattern in Android
Das Observer Pattern macht Zustandsänderungen sichtbar. Du erkennst es in Flow, Compose State und reaktiven Android-APIs.
Wenn du Android-Apps baust, ändern sich Daten ständig: ein Login läuft, eine Liste wird geladen, ein Button ist kurz deaktiviert, eine Fehlermeldung erscheint. Das Observer Pattern hilft dir, solche Änderungen sauber zu modellieren. Statt aktiv immer wieder nach neuen Daten zu fragen, meldet eine Datenquelle ihre Änderungen an interessierte Stellen. In modernem Android siehst du dieses Prinzip vor allem bei Kotlin Flow, Compose State und APIs, die den Android-Lebenszyklus beachten.
Was ist das?
Das Observer Pattern ist ein Entwurfsmuster für reaktive Kommunikation. Eine Quelle besitzt oder erzeugt Daten. Andere Teile deiner App interessieren sich für Änderungen an diesen Daten und registrieren sich als Beobachter. Wenn sich der Zustand ändert oder ein Ereignis auftritt, informiert die Quelle ihre Beobachter. Die Quelle muss dabei nicht genau wissen, was die Beobachter danach tun.
Das löst ein typisches Problem: Ohne dieses Muster würdest du viele direkte Abhängigkeiten bauen. Ein Repository müsste vielleicht wissen, welche Activity aktualisiert werden soll. Ein ViewModel müsste einzelne UI-Elemente direkt ansprechen. Eine Composable müsste selbst prüfen, ob sich Daten im Speicher geändert haben. Das macht Code schwer testbar und anfällig für Fehler.
Das bessere mentale Modell lautet: Daten fließen von einer Quelle zu Konsumenten. Die Quelle veröffentlicht Zustände oder Ereignisse. Konsumenten beobachten diesen Strom und reagieren passend. Im Android-Alltag können solche Konsumenten eine Compose-Oberfläche, ein ViewModel, ein Test oder ein anderer Service sein.
Wichtig ist die Unterscheidung zwischen Zustand und Ereignis. Ein Zustand beschreibt, was gerade gilt: loading, content, error, angemeldeter Nutzer, ausgewählter Filter. Ein Ereignis beschreibt, dass etwas passiert ist: Navigation, Snackbar, Klick, einmaliger Retry. Beide können beobachtbar sein, aber sie werden unterschiedlich behandelt. UI-Zustand sollte wiederholbar und stabil sein. Ereignisse dürfen nicht versehentlich mehrfach ausgelöst werden, nur weil die UI neu gezeichnet wurde.
Wie funktioniert es?
In klassischem objektorientiertem Code gibt es häufig eine Liste von Listenern: addListener, removeListener, notifyListeners. Android hatte lange viele solcher APIs, etwa Click-Listener oder Text-Watcher. Das Grundprinzip ist gleich: Eine Stelle registriert Interesse, eine andere Stelle sendet eine Benachrichtigung.
Modernes Android nutzt dafür oft strukturiertere Werkzeuge. Kotlin Flow beschreibt einen asynchronen Datenstrom. Ein Flow<T> kann mehrere Werte nacheinander ausgeben. Ein StateFlow<T> hält zusätzlich immer den aktuellen Wert. Das passt gut zu UI-Zustand, weil eine Oberfläche beim Start direkt den aktuellen Zustand braucht und danach weitere Änderungen beobachten kann.
Compose bringt ein eigenes Zustandsmodell mit. Wenn eine Composable State liest, merkt Compose sich diese Abhängigkeit. Ändert sich der State, kann Compose die betroffenen Teile der UI neu ausführen. Auch das ist Observer Pattern, nur nicht immer als explizite Listener-Liste sichtbar. Du schreibst deklarativ, welcher UI-Zustand angezeigt werden soll, und Compose reagiert auf Änderungen.
Der Android-Lebenszyklus ist dabei entscheidend. Eine Activity oder ein Screen ist nicht immer sichtbar. Wenn du Datenströme ohne Rücksicht auf den Lebenszyklus sammelst, kann deine App unnötig Arbeit machen oder UI-Updates senden, obwohl die Oberfläche gerade nicht aktiv ist. Deshalb solltest du UI-nahe Beobachtung an den Lifecycle koppeln, zum Beispiel über APIs wie collectAsStateWithLifecycle in Compose oder passende Lifecycle-Scopes in klassischem UI-Code.
Eine solide Architektur trennt außerdem Verantwortlichkeiten. Die Data Layer liefert Daten, etwa aus Netzwerk, Datenbank oder Cache. Das ViewModel bereitet daraus UI-Zustand auf. Die UI beobachtet diesen Zustand und rendert ihn. Diese Trennung ist kein Selbstzweck. Sie macht sichtbar, an welcher Stelle Daten entstehen, transformiert werden und am Bildschirm ankommen.
Für Anfänger ist eine Regel besonders hilfreich: Der Datenfluss sollte überwiegend in eine Richtung laufen. Die UI sendet Nutzerabsichten nach oben, etwa onRetryClicked() oder onNameChanged(value). Das ViewModel verarbeitet diese Absichten und aktualisiert den beobachtbaren UI-Zustand. Die UI beobachtet diesen Zustand wieder. So vermeidest du versteckte Rückkopplungen, bei denen ein UI-Update direkt neue Datenänderungen auslöst.
In der Praxis
Ein typischer Fall ist ein Screen, der eine Liste von Artikeln lädt. Das Repository stellt Daten bereit. Das ViewModel formt daraus einen stabilen UI-Zustand. Compose beobachtet diesen Zustand und zeigt Ladeanzeige, Inhalt oder Fehler.
data class Article(
val id: String,
val title: String
)
sealed interface ArticleUiState {
data object Loading : ArticleUiState
data class Content(val articles: List<Article>) : ArticleUiState
data class Error(val message: String) : ArticleUiState
}
class ArticleRepository {
fun observeArticles(): Flow<List<Article>> = flow {
emit(emptyList())
emit(
listOf(
Article("1", "Observer Pattern verstehen"),
Article("2", "Flow im ViewModel nutzen")
)
)
}
}
class ArticleViewModel(
private val repository: ArticleRepository
) : ViewModel() {
val uiState: StateFlow<ArticleUiState> =
repository.observeArticles()
.map<ArticleUiState> { articles ->
ArticleUiState.Content(articles)
}
.catch { error ->
emit(ArticleUiState.Error(error.message ?: "Unbekannter Fehler"))
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ArticleUiState.Loading
)
}
@Composable
fun ArticleScreen(
viewModel: ArticleViewModel
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
ArticleUiState.Loading -> {
CircularProgressIndicator()
}
is ArticleUiState.Content -> {
LazyColumn {
items(state.articles) { article ->
Text(text = article.title)
}
}
}
is ArticleUiState.Error -> {
Text(text = state.message)
}
}
}
Dieses Beispiel zeigt mehrere Dinge. Das Repository kennt die UI nicht. Das ViewModel kennt keine konkreten TextViews oder Compose-Layouts. Die Composable fragt nicht aktiv beim Repository nach. Stattdessen beobachtet sie uiState. Wenn der Flow einen neuen Wert liefert, wird der State aktualisiert und Compose rendert den passenden Zustand.
stateIn ist hier wichtig, weil aus einem Flow ein StateFlow wird. Ein StateFlow ist für UI-Zustand praktisch, weil immer ein aktueller Wert vorhanden ist. initialValue beschreibt, was angezeigt wird, bevor Daten eintreffen. SharingStarted.WhileSubscribed(5_000) sorgt dafür, dass die vorgelagerte Arbeit nicht dauerhaft aktiv bleiben muss, wenn niemand beobachtet. Der genaue Wert hängt von deiner App ab, aber die Idee bleibt: Beobachtung kostet Ressourcen, also behandelst du sie bewusst.
Eine typische Stolperfalle ist, Flow direkt in einer Composable ohne Lebenszyklusbewusstsein oder ohne stabile Struktur zu sammeln. Wenn du bei jeder Recompositon neue Flows erzeugst oder Nebenwirkungen direkt im Body einer Composable startest, kann Code mehrfach laufen. Das führt zu doppelten Requests, mehrfachen Snackbars oder schwer nachvollziehbaren Fehlern. Beobachte UI-Zustand deshalb möglichst über ViewModel-State und lebenszyklusbewusste Collect-APIs.
Eine zweite Stolperfalle betrifft einmalige Ereignisse. Navigation oder Toasts sollten nicht als dauerhafter UI-Zustand behandelt werden, der bei jeder neuen Beobachtung erneut ausgelöst wird. Wenn ein Screen nach einer Drehung wieder beobachtet, darf eine alte Navigation nicht erneut passieren. Für solche Fälle brauchst du ein klares Ereignismodell, zum Beispiel einen getrennten Event-Stream oder eine explizite Verbrauchslogik. Wichtig ist nicht der konkrete Mechanismus, sondern die saubere Entscheidung: Ist es ein Zustand, der erneut angezeigt werden darf, oder ein Ereignis, das nur einmal verarbeitet werden soll?
Auch bei Tests hilft dir das Muster. Du kannst das ViewModel testen, ohne eine echte UI zu starten. Gib dem Repository kontrollierte Werte und prüfe, ob der beobachtbare uiState die erwartete Reihenfolge liefert: zuerst Loading, dann Content, bei Fehlern Error. So testest du nicht nur einzelne Funktionen, sondern den Datenfluss. In Code-Reviews kannst du gezielt fragen: Wer besitzt den Zustand? Wer beobachtet ihn? Wird die Beobachtung passend beendet? Entstehen Nebenwirkungen nur an klaren Stellen?
Eine praktische Entscheidungsregel lautet: Alles, was die UI dauerhaft beschreiben soll, gehört als beobachtbarer UI-Zustand ins ViewModel. Alles, was nur eine einmalige Reaktion ist, braucht eine getrennte Behandlung. Alles, was aus Datenbank, Netzwerk oder Cache kommt, sollte möglichst als Stream gedacht werden, wenn spätere Änderungen relevant sind. So erkennst du das Observer Pattern nicht als abstraktes Schulbeispiel, sondern als Arbeitswerkzeug für Android-Code.
Fazit
Das Observer Pattern gibt dir ein klares Modell für reaktive Android-Apps: Eine Quelle veröffentlicht Änderungen, interessierte Stellen beobachten sie, und die UI reagiert auf stabilen Zustand statt auf direkte Kopplung. Übe das bewusst an einem kleinen Screen mit StateFlow und Compose State. Setze Breakpoints im Repository, im ViewModel und in der Composable, damit du den Weg eines Werts nachvollziehst. Ergänze danach einen Test für die Zustandsfolge und prüfe im Code-Review, ob Zustand, Ereignisse und Lifecycle sauber getrennt sind.