Android Coden
Android 4 min lesen

Verantwortlichkeiten der UI-Schicht

Die UI-Schicht hat zwei klar abgegrenzte Aufgaben: Zustand darstellen und Nutzer-Events weiterleiten. Wer das verinnerlicht, schreibt wartbaren Android-Code.

Wer eine Android-App entwickelt, stößt früh auf die Frage, wo welche Logik hingehört. Die offizielle Jetpack-Architektur gibt darauf eine klare Antwort: Die UI-Schicht ist nicht der Ort für Berechnungen, Datenbankabfragen oder Netzwerkkommunikation. Ihre einzige Aufgabe ist es, den aktuellen Zustand der App sichtbar zu machen und Aktionen des Nutzers nach oben weiterzureichen. Dieser einfache Grundsatz hat weitreichende Konsequenzen für die Struktur deiner Codebase.

Was ist das?

Die UI-Schicht ist der Teil deiner App, den der Nutzer sieht und bedient. In der Jetpack-Architektur umfasst sie alle Composables, Activities und Fragments sowie die zugehörigen ViewModels. Google definiert die Verantwortlichkeiten dieser Schicht mit zwei präzisen Aussagen: Sie rendert Zustand, und sie leitet Events weiter.

Zustand rendern bedeutet, dass die Oberfläche immer ein 1:1-Abbild eines definierten Datenzustands ist. Jede sichtbare Information – ein Ladespinner, ein Fehlertext, eine Liste von Beiträgen – ergibt sich direkt aus einem UiState-Objekt. Die UI entscheidet nicht selbst, was angezeigt wird; sie übersetzt lediglich Daten in Pixel.

Events weiterleiten bedeutet, dass Klicks, Swipes oder Texteingaben nicht von der UI selbst verarbeitet werden. Sie meldet der darunterliegenden Schicht, dass etwas passiert ist – und wartet auf die nächste Zustandsänderung. Die UI ist in diesem Modell passiv: Sie reagiert, sie handelt nicht.

Diese strikte Aufteilung ist kein akademisches Ideal, sondern eine praktische Notwendigkeit. Sobald ein Composable eigene Logik enthält, wird es schwer testbar, schwer wiederverwendbar und eine Quelle von Inkonsistenzen, wenn dieselbe Regel an mehreren Stellen unterschiedlich implementiert wird.

Wie funktioniert es?

Der tragende Mechanismus ist der Unidirectional Data Flow (UDF). Daten fließen in genau eine Richtung: vom ViewModel zur UI. Events fließen entgegengesetzt: von der UI zum ViewModel. Dieses Muster ist in der Android-Architektur-Dokumentation als empfohlener Standard festgelegt.

Der Ablauf in vier Schritten:

  1. Das ViewModel hält einen StateFlow<UiState> und aktualisiert ihn, wenn sich Daten ändern.
  2. Das Composable sammelt den Flow mit collectAsStateWithLifecycle() und rendert den aktuellen Wert.
  3. Nutzeraktionen werden als Methodenaufrufe oder als UiEvent-Objekte an das ViewModel übergeben.
  4. Das ViewModel aktualisiert den Zustand – die UI reagiert automatisch durch Recomposition.

UiState modellieren

Modelliere deinen Zustand als data class, wenn der gesamte Bildschirm denselben strukturellen Aufbau hat. Verwende ein sealed interface, wenn der Bildschirm zwischen grundlegend verschiedenen Zuständen wechselt – etwa zwischen Loading, Success und Error:

sealed interface ArticleUiState {
    data object Loading : ArticleUiState
    data class Success(val articles: List<Article>) : ArticleUiState
    data class Error(val message: String) : ArticleUiState
}

Das ViewModel publiziert diesen Zustand über einen StateFlow und verarbeitet eingehende Events:

class ArticleViewModel(private val repository: ArticleRepository) : ViewModel() {
    private val _uiState = MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)
    val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()

    init {
        loadArticles()
    }

    private fun loadArticles() {
        viewModelScope.launch {
            _uiState.value = try {
                ArticleUiState.Success(repository.getArticles())
            } catch (e: Exception) {
                ArticleUiState.Error(e.localizedMessage ?: "Unbekannter Fehler")
            }
        }
    }

    fun onRetryClicked() {
        _uiState.value = ArticleUiState.Loading
        loadArticles()
    }
}

Der entscheidende Vorteil: Jeder Zustand der Oberfläche ist jederzeit vollständig im UiState-Objekt beschrieben und lässt sich ohne laufende UI in einem Unit-Test überprüfen.

In der Praxis

Das Composable konsumiert den Zustand und leitet Events weiter – es berechnet nichts selbst:

@Composable
fun ArticleScreen(viewModel: ArticleViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when (val state = uiState) {
        is ArticleUiState.Loading -> CircularProgressIndicator()
        is ArticleUiState.Success -> ArticleList(articles = state.articles)
        is ArticleUiState.Error -> ErrorView(
            message = state.message,
            onRetry = viewModel::onRetryClicked
        )
    }
}

ArticleScreen trifft keine einzige inhaltliche Entscheidung. Es weiß nicht, woher die Daten kommen oder warum ein Fehler aufgetreten ist. Es rendert ausschließlich das, was der Zustand vorschreibt.

Typische Stolperfalle: Logik im Composable

Die häufigste Verletzung der Schichtverantwortlichkeit sieht so aus:

// FALSCH
@Composable
fun ArticleScreen(articles: List<Article>) {
    val recent = articles.filter { it.publishedAt.isAfter(LocalDate.now().minusDays(7)) }
    ArticleList(articles = recent)
}

Das Filtern nach Datum ist Geschäftslogik – sie gehört ins ViewModel oder in einen Use Case. Sobald sie im Composable sitzt, wird sie bei jedem Recompose erneut ausgeführt, ist nicht isoliert testbar und führt zu Inkonsistenzen, sobald eine zweite Stelle dieselbe Regel anders implementiert.

Die praktische Faustregel lautet: Wenn du im Composable etwas berechnest, das nicht unmittelbar der Darstellung dient, gehört es ins ViewModel. Das Composable darf Werte formatieren (String.format, pluralStringResource), aber nicht filtern, aggregieren oder entscheiden.

Fazit

Die UI-Schicht hat eine einzige, klar abgegrenzte Aufgabe: Zustand sichtbar machen und Events weiterleiten. Diese Trennung ist kein Selbstzweck – sie macht deine App testbarer, dein Team effizienter und Bugs seltener. Prüfe in deiner nächsten Code-Review-Runde gezielt, ob sich Berechnungen oder bedingte Logik in Composables eingeschlichen haben. Schreibe anschließend einen Unit-Test für dein ViewModel, der den UiState direkt überprüft, ohne dass eine UI gestartet werden muss. Wenn dieser Test gelingt und alle relevanten Zustände abdeckt, ist die Schichttrennung korrekt umgesetzt.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

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