Android Coden
Android 4 min lesen

UI-State-Modellierung

Lerne, wie du den UI-State einer App explizit modellierst. Vorhersagbare Rendering-Logik beginnt mit klaren Zustandsdefinitionen.

Jedes Mal, wenn eine App Daten lädt, einen Fehler anzeigt oder fertige Inhalte rendert, befindet sich die Oberfläche in einem definierten Zustand. UI-State-Modellierung bedeutet, diese Zustände explizit im Code zu beschreiben – statt sie implizit über verstreute Flags und nullable Variablen entstehen zu lassen. Wer diesen Schritt konsequent geht, schreibt Rendering-Logik, die sich vorhersagbar verhält und sich leicht testen lässt.

Was ist das?

UI-State-Modellierung ist das bewusste Entwerfen einer Datenstruktur, die den vollständigen Zustand eines Screens zu einem bestimmten Zeitpunkt beschreibt. Statt isLoading: Boolean, errorMessage: String? und data: List<Item>? als einzelne Felder nebeneinander zu halten, fasst du alle diese Informationen in einem einzigen, klar benannten Objekt zusammen.

In der offiziellen Android-Architektur lebt dieses Objekt in der UI-Schicht und wird vom ViewModel verwaltet. Die Compose-UI oder ein Fragment beobachtet es und rendert passend dazu. Der Datenfluss ist dabei immer unidirektional: Das ViewModel produziert State, die UI konsumiert ihn – nie umgekehrt.

Das Modell selbst ist entweder eine data class für einfache Screens oder eine sealed class, wenn sich der Screen grundlegend unterschiedlich aussieht, je nachdem ob Daten vorhanden, noch unterwegs oder fehlerhaft sind. Die sealed class hat einen entscheidenden Vorteil: Sie macht die möglichen Zustände auf Typ-Ebene sichtbar und zwingt den Compiler, alle Fälle in einem when-Ausdruck abzudecken. Vergisst du einen Zustand, bricht der Build – kein stiller Laufzeitfehler.

Wie funktioniert es?

Das Herzstück ist ein StateFlow<UiState> im ViewModel. Jedes Mal, wenn sich die Datenlage ändert, emittiert das ViewModel einen neuen, vollständigen State-Snapshot – niemals einen partiellen Update auf ein einzelnes Feld. Die UI abonniert diesen Flow und rendert ausschließlich auf Basis des aktuellen Werts.

// State-Definition
sealed class ArticleUiState {
    object Loading : ArticleUiState()
    data class Error(val message: String) : ArticleUiState()
    data class Content(val articles: List<Article>) : ArticleUiState()
}

// ViewModel
class ArticleViewModel(private val repo: ArticleRepository) : ViewModel() {

    private val _uiState = MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)
    val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()

    init { loadArticles() }

    private fun loadArticles() {
        viewModelScope.launch {
            _uiState.value = ArticleUiState.Loading
            try {
                val articles = repo.fetchArticles()
                _uiState.value = ArticleUiState.Content(articles)
            } catch (e: Exception) {
                _uiState.value = ArticleUiState.Error(e.localizedMessage ?: "Unbekannter Fehler")
            }
        }
    }
}

In Compose sammelst du den State mit collectAsStateWithLifecycle() (aus dem Artifact androidx.lifecycle:lifecycle-runtime-compose). Die Funktion respektiert den Lifecycle und stoppt die Collection automatisch, wenn die Composable nicht mehr aktiv ist – wichtig für die Akku-Laufzeit.

Unveränderlichkeit ist keine Stilfrage, sondern eine technische Notwendigkeit. Wird ein State-Objekt außerhalb des ViewModels mutiert, erkennt Compose die Änderung nicht, weil StateFlow auf Referenzgleichheit prüft. Nutze daher immer data class mit copy() und niemals var-Felder in der State-Klasse.

In der Praxis

Composable konsumiert den State

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

    when (state) {
        is ArticleUiState.Loading -> CircularProgressIndicator()
        is ArticleUiState.Error   -> ErrorMessage((state as ArticleUiState.Error).message)
        is ArticleUiState.Content -> ArticleList((state as ArticleUiState.Content).articles)
    }
}

Die Composable selbst hat keine Logik – sie nimmt entgegen und rendert. Das macht sie leicht per @Preview testbar und unabhängig vom ViewModel austauschbar.

Typische Stolperfalle: mehrere einzelne StateFlows

Ein häufiger Anfängerfehler ist, Loading-, Error- und Content-Informationen auf drei separate MutableStateFlow-Felder aufzuteilen. Das führt unweigerlich zu inkonsistenten Kombinationen – zum Beispiel isLoading = true und errorMessage != null gleichzeitig. Welche Variante soll die UI dann anzeigen? Ein einziger Flow mit klar modelliertem Typen macht ungültige Kombinationen schlicht unmöglich; die sealed class ist das Typsystem, das für dich denkt.

ViewModel-Logik ohne Emulator testen

Weil der State ein einfaches Kotlin-Objekt ist, lässt sich das ViewModel in einem reinen JUnit-Test prüfen:

@Test
fun `lädt Artikel und wechselt in Content-State`() = runTest {
    val fakeRepo = FakeArticleRepository(listOf(Article("Kotlin Flows")))
    val vm = ArticleViewModel(fakeRepo)

    assertEquals(ArticleUiState.Loading, vm.uiState.value)

    advanceUntilIdle()

    assertTrue(vm.uiState.value is ArticleUiState.Content)
}

Kein Emulator, kein Fragment, kein Instrumented Test nötig. Die offiziellen Architektur-Empfehlungen betonen genau diesen Vorteil: Eine klar getrennte UI-Schicht mit explizitem State erlaubt schnelle, stabile Unit-Tests, die im CI-Durchlauf in Sekunden laufen statt in Minuten.

Fazit

Explizite UI-State-Modellierung ist einer der wirksamsten Schritte auf dem Weg zu wartbarem Android-Code. Sie zwingt dich, alle möglichen Zustände eines Screens durchzudenken, bevor du die erste Composable schreibst – das Ergebnis sind weniger ungeklärte Randfälle, klarere Rendering-Logik und ein ViewModel, das sich vollständig ohne UI testen lässt. Öffne jetzt ein bestehendes ViewModel in deinem Projekt und suche nach mehreren einzelnen State-Feldern, die sich zu einer sealed class zusammenfassen lassen. Schreib danach einen Unit-Test, der alle drei Zustände (Loading, Error, Content) durchläuft – das ist der schnellste Weg, den Wert dieser Technik direkt am eigenen Code zu erleben.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

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