ViewModel-Verantwortlichkeiten
Das ViewModel hält UI-Zustand vor und koordiniert Geschäftslogik. Du lernst, was hineingehört – und was nicht.
Das ViewModel ist eine der zentralen Klassen im Android-Jetpack-Architektur-Toolkit. Es sitzt zwischen deiner UI-Schicht und der Datenschicht, bereitet Zustand für die Oberfläche auf und koordiniert fachliche Abläufe – ohne dabei selbst an den Android-Lifecycle gebunden zu sein. Wer versteht, was in ein ViewModel gehört und was nicht, legt das Fundament für wartbare, testbare Android-Apps.
Was ist das?
Ein ViewModel ist ein lifecycle-bewusster Zustandshalter für genau einen Screen oder eine klar abgegrenzte UI-Komponente. Google definiert seine Hauptaufgaben in der offiziellen Architektur-Doku kurz und bündig: Zustand für die UI aufbereiten, auf Nutzerinteraktionen reagieren und diesen Zustand über Konfigurationsänderungen hinweg erhalten.
Der entscheidende Unterschied zu einer einfachen Klasse liegt im Lebenszyklus: Ein ViewModel wird erzeugt, wenn ein Screen das erste Mal gestartet wird, und erst dann zerstört, wenn der Screen wirklich entfernt wird – nicht bei jeder Rotation. Activities und Fragments dagegen werden bei jeder Konfigurationsänderung neu erzeugt. Würdest du Netzwerk-Anfragen oder teure Berechnungen direkt in der Activity starten, müsstest du sie bei jeder Rotation neu anstoßen und das Ergebnis erneut laden. Das ViewModel löst dieses Problem systematisch, ohne dass du selbst Instanzen irgendwo cachen musst.
Im Compose-Kontext wird ein ViewModel typischerweise über viewModel() oder hiltViewModel() bereitgestellt und hält einen StateFlow<UiState>, den die Composable via collectAsStateWithLifecycle() beobachtet. Ändert sich der Zustand, rendert Compose die betroffenen Bereiche des Screens automatisch neu.
Wie funktioniert es?
Das ViewModel sitzt in der UI-Schicht der empfohlenen Android-Architektur. Es kommuniziert nach unten mit der Domänen- oder Datenschicht über Repositories oder UseCases und nach oben mit der Composable- oder Fragment-Schicht über beobachtbare Datenströme.
State-Holder-Rolle: Das ViewModel hält den aktuellen UI-Zustand als einzige Quelle der Wahrheit. Empfohlen wird ein versiegeltes Interface oder eine Data-Class UiState, die alle relevanten Felder des Screens beschreibt – Ladezustand, Fehlerzustand, Inhalt. So weißt du auf einen Blick, was die UI gerade darstellt, und kannst den Zustand in Tests leicht überprüfen.
Business-Koordination: Das ViewModel ruft Repositories oder UseCases auf, transformiert deren Ergebnisse in UI-freundliche Formate und schreibt das Ergebnis in den MutableStateFlow. Es ist der Koordinator zwischen Geschäftslogik und Darstellung – aber es ist nicht selbst die Geschäftslogik. Komplexe Berechnungen oder Datenbankoperationen gehören ins Repository oder in einen UseCase, nicht ins ViewModel.
Lifecycle-Kopplung über viewModelScope: Jede Coroutine, die das ViewModel startet, sollte in viewModelScope laufen. Dieser Scope wird automatisch beendet, wenn das ViewModel gelöscht wird – Ressourcenlecks durch laufende Coroutinen nach dem Entfernen eines Screens sind damit ausgeschlossen.
In der Praxis
Ein typisches ViewModel für einen Artikel-Screen könnte so aussehen:
@HiltViewModel
class ArticleViewModel @Inject constructor(
private val repository: ArticleRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val articleId: String = checkNotNull(savedStateHandle["articleId"])
private val _uiState = MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)
val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()
init {
loadArticle()
}
private fun loadArticle() {
viewModelScope.launch {
_uiState.value = try {
val article = repository.getArticle(articleId)
ArticleUiState.Success(article)
} catch (e: Exception) {
ArticleUiState.Error(e.localizedMessage ?: "Unbekannter Fehler")
}
}
}
fun onRetryClicked() {
_uiState.value = ArticleUiState.Loading
loadArticle()
}
}
sealed interface ArticleUiState {
data object Loading : ArticleUiState
data class Success(val article: Article) : ArticleUiState
data class Error(val message: String) : ArticleUiState
}
Einige wichtige Details:
SavedStateHandlestatt Intent-Extras: Navigationsargumente kommen sauber überSavedStateHandleins ViewModel, ohne dass eine Referenz auf die Activity benötigt wird.- Nur
asStateFlow()exponieren: Der interneMutableStateFlowbleibt privat; die Composable sieht ausschließlich die Read-only-Variante.
Häufige Stolperfalle: Context im ViewModel
Der klassische Fehler ist das Speichern eines Context-Objekts als Feld im ViewModel. Weil das ViewModel die Activity überlebt, hält es dabei eine Referenz auf die alte Activity-Instanz – ein sicherer Memory-Leak. Wenn du String-Ressourcen auflösen musst, gibt es zwei saubere Alternativen: Entweder du nutzt AndroidViewModel, das einen Application-Context bekommt (kein Leak, weil der Application-Context nicht zerstört wird), oder – noch besser – du gibst aus dem ViewModel nur IDs oder Enum-Werte zurück und überlässt die String-Auflösung der UI-Schicht.
Was gehört nicht ins ViewModel?
- Direkte Referenzen auf
View,Activity,Fragmentoder regulärenContext - Datenbankoperationen oder HTTP-Requests (diese gehören ins Repository)
- Navigationslogik (nutze stattdessen einen dedizierten NavigationManager oder Channel-basierte Events)
- Steuerung von UI-Animationen oder Scroll-Positionen
Fazit
Das ViewModel ist der Dreh- und Angelpunkt der UI-Schicht: Es hält den Zustand durch Konfigurationsänderungen hindurch, koordiniert Aufrufe an tiefere Schichten und stellt der UI einen sauberen, beobachtbaren Datenstrom bereit. Die Grenzen seines Zuständigkeitsbereichs sind dabei genauso wichtig wie seine Fähigkeiten – ein ViewModel, das zu viel weiß, wird schnell zum Flaschenhals und ist schwer zu testen. Nimm dir nach dieser Einheit einen bestehenden Screen deiner App vor: Prüfe, ob dein ViewModel direkten Zugriff auf Views oder Context hat, ob Geschäftslogik ins Repository ausgelagert ist, und schreibe einen einfachen Unit-Test mit einem gefakten Repository. Wenn der Test grün ist und das ViewModel kein Android-Framework importiert, weißt du, dass deine Verantwortlichkeiten klar getrennt sind.