Single Source of Truth: Konsistente Zustände in Android
Das Single-Source-of-Truth-Prinzip bedeutet: Ein Zustand, eine Quelle. Es verhindert Datenwidersprüche und ist die Basis für offline-fähige Android-Apps.
Stell dir vor, du öffnest eine App und siehst deinen Kontostand in der Übersicht korrekt angezeigt – aber sobald du in die Detailansicht wechselst, steht dort ein anderer Betrag. Solche Widersprüche entstehen, wenn ein Zustand an mehreren Stellen gleichzeitig gepflegt wird. Das Prinzip der Single Source of Truth (SSOT) löst genau dieses Problem: Jeder Zustand hat genau eine autoritative Quelle, und alle anderen Teile der App lesen nur von dort.
Was ist das?
Single Source of Truth ist kein Framework und kein konkretes API, sondern ein Architekturprinzip. Es besagt, dass jede Information in deiner App – ob Benutzerprofil, Liste geladener Artikel oder ein einzelnes Formularfeld – nur an einem einzigen Ort gespeichert und verwaltet wird. Alle anderen Komponenten, die diesen Zustand benötigen, beobachten ihn passiv, anstatt ihn selbst zu halten.
In Android-Apps manifestiert sich dieses Prinzip typischerweise in der Datenschicht. Dein Repository ist die SSOT für die Geschäftsdaten deiner App. Es entscheidet, welche Daten angezeigt werden – ob aus dem Netzwerk, aus dem lokalen Cache oder aus einer kombinierten Quelle. Das ViewModel erhält den Zustand vom Repository und leitet ihn an die UI weiter, ohne ihn neu zu interpretieren. Die UI zeigt ausschließlich das an, was ihr präsentiert wird.
Dieses Prinzip ist eng mit dem übergreifenden Android-Architekturmodell verknüpft, das Google in seinen offiziellen Empfehlungen beschreibt: Datenfluss ist unidirektional – von der Datenquelle nach oben zur UI. Zustandsänderungen gehen stets den umgekehrten Weg: von der UI als Intention hinunter zur Datenschicht. SSOT ist die strukturelle Voraussetzung dafür, dass dieser unidirektionale Fluss in der Praxis funktioniert.
Wie funktioniert es?
Die technische Umsetzung in modernen Android-Apps baut auf drei Bausteinen auf.
Room als lokale Datenbank
Room ist die empfohlene Persistenzschicht und in der Regel die SSOT für alle lokalen Daten. Wenn du einen Datensatz in Room änderst, reflektiert die gesamte App automatisch diese Änderung – vorausgesetzt, du beobachtest die Daten reaktiv über einen Flow.
Repository koordiniert alle Quellen
Das Repository kombiniert Remote-Daten (API) und lokale Daten (Room). Gemäß der Offline-first-Empfehlung von Google persistiert das Repository Netzwerkdaten immer zuerst in Room und exponiert anschließend ausschließlich den Room-Flow gegenüber dem ViewModel. Die Netzwerkschicht ist damit nicht die SSOT, sondern lediglich ein Zulieferer für die Datenbank.
class ArticleRepository(
private val api: ArticleApi,
private val dao: ArticleDao
) {
val articles: Flow<List<Article>> = dao.observeAll()
suspend fun refresh() {
val remoteArticles = api.fetchArticles()
dao.upsertAll(remoteArticles)
}
}
In diesem Beispiel ist dao.observeAll() die einzige Quelle, die das ViewModel beobachtet. refresh() schreibt Netzwerkdaten in Room, und Room informiert dann automatisch alle Beobachter. Die UI bekommt niemals direkt Daten aus dem Netzwerk.
StateFlow im ViewModel
Das ViewModel hält den Zustand als StateFlow. Es transformiert den Repository-Flow bei Bedarf, legt aber keine eigene Kopie der Rohdaten an.
class ArticleViewModel(private val repository: ArticleRepository) : ViewModel() {
val articles: StateFlow<List<Article>> = repository.articles
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
}
Die Compose-UI liest articles über collectAsStateWithLifecycle() und rendert jedes Mal neu, wenn sich der Wert ändert – ohne selbst Zustand zu halten.
In der Praxis
Häufige Stolperfalle: Zustand duplizieren
Der verbreitetste Verstoß gegen SSOT sieht so aus: Du hast eine Liste von Artikeln im ViewModel als StateFlow. Beim Filtern erstellst du eine zweite MutableStateFlow-Liste mit den gefilterten Ergebnissen und pflegst beide separat. Schon hast du zwei Quellen für dasselbe Konzept. Fügt der Nutzer nun ein Element hinzu, muss es in beide Listen eingetragen werden – und der erste Bug ist vorprogrammiert.
Die Lösung lautet immer: eine Quelle, abgeleitete Zustände. Der Filter ist kein eigenständiger Zustand, sondern ein Parameter, der auf die eine Quelle angewendet wird:
private val _filter = MutableStateFlow("")
val filteredArticles: StateFlow<List<Article>> = combine(
repository.articles,
_filter
) { articles, filter ->
if (filter.isBlank()) articles
else articles.filter { it.title.contains(filter, ignoreCase = true) }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
_filter und repository.articles sind jeweils unabhängige, autoritative Quellen für ihr eigenes Konzept. Das kombinierte Ergebnis ist abgeleitet und wird nie separat gespeichert.
Offline-first als direkte Konsequenz
Wenn Room deine SSOT ist, bekommst du Offline-Unterstützung fast automatisch. Die App zeigt immer gecachte Daten an, auch ohne Netzwerkverbindung. refresh() schlägt dann fehl – du zeigst einen Fehlerhinweis –, aber die bestehenden Daten bleiben sichtbar. Wärst du ohne lokale SSOT direkt auf die API angewiesen, würde die App bei fehlender Verbindung einfach leer bleiben oder abstürzen.
Fazit
Das Single-Source-of-Truth-Prinzip ist eine der wirkungsvollsten Leitlinien in der Android-Architektur – nicht weil es die Implementierung vereinfacht, sondern weil es dich zwingt, bewusst zu entscheiden, wo ein Zustand lebt und wer ihn verändern darf. Nimm dir jetzt Zeit, dein aktuelles Projekt unter diesem Blickwinkel zu prüfen: Gibt es ViewModels, die denselben Zustand zweimal halten? Holt deine UI direkt Daten aus dem Netzwerk, ohne sie erst lokal zu persistieren? Jeder dieser Funde ist ein konkreter Ansatzpunkt für einen Refactor – und ein guter Beweis dafür, ob du das Prinzip wirklich verstanden hast.