Android Coden
Android 8 min lesen

Problemzerlegung in Android-Projekten

Du lernst, Features sauber in Teilprobleme zu zerlegen. So planst du Android-Code prüfbar und wartbar.

Problem Decomposition bedeutet, ein größeres Entwicklungsproblem in kleinere, klar benannte Teilprobleme zu zerlegen. In Android-Projekten hilft dir das besonders bei Features, die auf den ersten Blick simpel wirken, aber Daten, UI, Zustand, Fehlerfälle und Tests berühren. Statt sofort eine Composable, ein ViewModel oder eine Repository-Methode zu schreiben, klärst du zuerst, welche Aufgaben das Feature wirklich hat und welche Anforderungen daraus folgen.

Was ist das?

Problemzerlegung ist eine Denk- und Entwurfstechnik. Du nimmst eine Feature-Idee wie „Der Nutzer kann Aufgaben als erledigt markieren“ und zerlegst sie in konkrete Verantwortungen. Welche Daten werden gelesen oder verändert? Welche UI zeigt den Zustand? Welcher State entsteht während Laden, Erfolg und Fehler? Welche Fehler sind sichtbar? Welche Tests beweisen, dass das Verhalten stimmt?

Für Anfänger ist das wichtigste mentale Modell: Ein Feature ist selten eine einzelne Datei. Es ist eine Kette aus Entscheidungen. Daten kommen aus einer Quelle, werden in einen UI-Zustand übersetzt, von Compose dargestellt, durch Nutzeraktionen verändert und durch Tests abgesichert. Wenn du diese Kette nicht bewusst trennst, entsteht schnell Code, der zwar funktioniert, aber schwer zu ändern ist.

Im Android-Kontext passt Problem Decomposition direkt zu Kotlin, Jetpack Compose und Architecture Components. Compose beschreibt UI als Funktion von State. Das bedeutet: Deine Oberfläche sollte nicht raten, was gerade passiert. Sie sollte einen klaren Zustand erhalten, zum Beispiel Loading, Content oder Error. Das ViewModel koordiniert diesen Zustand, während Repository oder Use Case für Datenarbeit zuständig sind. Diese Trennung ist kein Selbstzweck. Sie macht Anforderungen sichtbarer und reduziert Überraschungen beim Testen, Debuggen und späteren Erweitern.

Problemzerlegung gehört damit zu den Software-Engineering-Grundlagen. Sie verbindet Requirements und Design: Anforderungen sagen, was beobachtbar passieren soll. Design entscheidet, wie du Verantwortungen im Code verteilst. Gute Android-Entwicklung beginnt nicht mit möglichst vielen Klassen, sondern mit klaren Grenzen zwischen Aufgaben.

Wie funktioniert es?

Du kannst Problem Decomposition als festen Ablauf verwenden. Starte mit dem sichtbaren Verhalten. Beschreibe in ein bis drei Sätzen, was der Nutzer tun kann und was danach auf dem Bildschirm passiert. Danach zerlegst du das Verhalten in fünf Bereiche: Daten, UI, State, Fehler und Tests.

Der Datenbereich beantwortet Fragen wie: Woher kommen die Informationen? Werden sie lokal gespeichert, über ein Netzwerk geladen oder nur im Speicher gehalten? Gibt es eine ID, ein Datum, einen Status oder eine Sortierung? Hier entstehen oft Repository-Methoden, Datenklassen oder Mapper. Wichtig ist, dass du nicht sofort UI-Details in die Datenebene ziehst. Eine Datenklasse sollte nicht wissen, ob ein Text später grün oder rot angezeigt wird.

Der UI-Bereich beschreibt, welche sichtbaren Elemente nötig sind. In Compose sind das Composables, die möglichst wenig eigene Geschäftslogik enthalten. Eine Composable bekommt State und Callbacks. Sie entscheidet nicht selbst, ob ein Vorgang fachlich erlaubt ist, sondern zeigt an, was ihr übergeben wird. So bleibt sie leichter in Previews und UI-Tests prüfbar.

Der State-Bereich ist der zentrale Übergang zwischen Daten und UI. Hier definierst du, welche Zustände das Feature haben kann. Ein häufiger Fehler ist, mehrere lose Boolean-Werte zu verteilen: isLoading, hasError, isEmpty, isSaving. Das wirkt anfangs bequem, führt aber schnell zu widersprüchlichen Kombinationen. Besser ist oft ein eigener UI-State, der die erlaubten Zustände ausdrückt. Compose profitiert davon, weil Recomposition verständlicher wird: Wenn sich State ändert, wird die UI neu beschrieben.

Der Fehlerbereich gehört ausdrücklich zur Problemzerlegung. Fehler sind keine Ergänzung am Schluss. Sie sind Teil der Anforderung. Was passiert bei fehlendem Netzwerk? Was passiert, wenn Speichern fehlschlägt? Bleibt die alte Ansicht sichtbar? Zeigst du eine Snackbar, einen Inline-Fehler oder einen leeren Zustand? Wer diese Fragen früh klärt, vermeidet unklare Nutzerführung und schwer testbare Sonderfälle.

Der Testbereich fragt: Wie beweist du, dass das Feature korrekt arbeitet? Nicht jeder Test muss ein großer End-to-End-Test sein. Android Testing Fundamentals unterscheiden zwischen Tests auf verschiedenen Ebenen. Für ein zerlegtes Feature kannst du gezielt entscheiden: Die State-Logik testest du als lokalen Unit-Test. Eine Composable prüfst du mit einem UI-Test. Datenzugriffe testest du mit Fakes oder Test-Doubles. Android-Qualität entsteht nicht durch viele zufällige Tests, sondern durch Tests an den richtigen Grenzen.

Eine praktische Regel lautet: Wenn du ein Feature nicht in fünf Stichpunkten nach Daten, UI, State, Fehlern und Tests beschreiben kannst, ist es noch nicht bereit für die Implementierung. Du musst nicht alles perfekt planen. Aber du solltest die Hauptverantwortungen kennen, bevor du Code schreibst.

In der Praxis

Nimm ein kleines Feature: Eine Aufgabenliste soll Aufgaben laden und erlauben, eine Aufgabe als erledigt zu markieren. Ohne Problemzerlegung würdest du vielleicht direkt in einer Composable eine Liste bauen, dort einen Klick behandeln und nebenbei Daten ändern. Das funktioniert für eine Demo. In einer echten App vermischen sich dann UI, State und Datenzugriff.

Zerlegt sieht dasselbe Feature so aus:

Daten: Es gibt eine Task mit id, title und done. Ein Repository lädt Aufgaben und aktualisiert den Status.

UI: Eine Liste zeigt Titel und Checkbox. Bei leerer Liste gibt es einen passenden leeren Zustand. Während des Ladens gibt es eine Ladeanzeige.

State: Die Oberfläche kennt mindestens Loading, Content, Empty und Error. Beim Ändern einer Aufgabe kann zusätzlich ein Speichervorgang laufen.

Fehler: Wenn Laden fehlschlägt, wird eine Fehlermeldung mit Wiederholen-Aktion gezeigt. Wenn Speichern fehlschlägt, bleibt die Aufgabe sichtbar und der Nutzer bekommt eine Rückmeldung.

Tests: Die ViewModel-Logik wird mit einem Fake-Repository getestet. Die UI wird mit festem State gerendert und auf sichtbare Texte oder Aktionen geprüft.

Ein kompakter Kotlin-Entwurf kann so aussehen:

data class Task(
    val id: String,
    val title: String,
    val done: Boolean
)

sealed interface TaskListUiState {
    data object Loading : TaskListUiState
    data class Content(val tasks: List<Task>) : TaskListUiState
    data object Empty : TaskListUiState
    data class Error(val message: String) : TaskListUiState
}

interface TaskRepository {
    suspend fun loadTasks(): List<Task>
    suspend fun setDone(id: String, done: Boolean)
}

class TaskListViewModel(
    private val repository: TaskRepository
) : ViewModel() {

    private val _uiState =
        MutableStateFlow<TaskListUiState>(TaskListUiState.Loading)

    val uiState: StateFlow<TaskListUiState> = _uiState.asStateFlow()

    fun load() {
        viewModelScope.launch {
            _uiState.value = TaskListUiState.Loading
            _uiState.value = try {
                val tasks = repository.loadTasks()
                if (tasks.isEmpty()) {
                    TaskListUiState.Empty
                } else {
                    TaskListUiState.Content(tasks)
                }
            } catch (error: IOException) {
                TaskListUiState.Error("Aufgaben konnten nicht geladen werden.")
            }
        }
    }

    fun setDone(task: Task, done: Boolean) {
        viewModelScope.launch {
            try {
                repository.setDone(task.id, done)
                load()
            } catch (error: IOException) {
                _uiState.value =
                    TaskListUiState.Error("Änderung konnte nicht gespeichert werden.")
            }
        }
    }
}

Dieser Code ist nicht vollständig für jede Produktions-App, aber er zeigt die Aufteilung. Die UI muss nicht wissen, wie Aufgaben geladen werden. Das Repository muss nicht wissen, wie Compose rendert. Das ViewModel übersetzt Daten und Fehler in einen Zustand, den die Oberfläche darstellen kann. Genau diese Übersetzung ist häufig der Ort, an dem gute Android-Architektur sichtbar wird.

In Compose würdest du den State beobachten und abhängig vom Typ darstellen. Die Composable sollte Callbacks wie onRetry oder onTaskChecked annehmen. Dadurch kannst du sie mit Testdaten rendern, ohne echtes Netzwerk, echte Datenbank oder komplexe Einrichtung. Das ist ein direkter Nutzen der Problemzerlegung: Du kannst Teile einzeln prüfen.

Eine typische Stolperfalle ist die zu frühe technische Lösung. Viele Junior-Devs fragen zuerst: „Brauche ich Room, Retrofit, Flow, LiveData oder DataStore?“ Die bessere erste Frage lautet: „Welche Aufgabe löst dieses Feature, und welche Zustände muss der Nutzer sehen?“ Erst danach entscheidest du, welche API passt. Sonst baust du schnell eine technisch aufwendige Struktur, die die eigentliche Anforderung nur unklar ausdrückt.

Eine zweite Stolperfalle ist die Vermischung von Anforderungen und Darstellung. „Der Button ist blau“ ist eine UI-Entscheidung. „Der Nutzer kann nur speichern, wenn der Titel nicht leer ist“ ist eine fachliche Regel. Diese Regel sollte nicht nur in einem Button-enabled stecken, sondern in der State- oder ViewModel-Logik nachvollziehbar sein. Dann kannst du sie testen und im Code-Review gezielt besprechen.

Für deinen Alltag lohnt sich ein kleiner Arbeitsablauf vor jedem Feature-Branch:

Feature-Skizze

Schreibe zuerst eine kurze Feature-Skizze. Sie muss nicht länger als eine halbe Seite sein. Notiere die sichtbaren Nutzeraktionen, die Datenquellen, die UI-Zustände, die Fehlerfälle und die Tests. Diese Skizze schützt dich vor blindem Implementieren und macht Pull Requests verständlicher.

State zuerst benennen

Benenne danach die UI-Zustände. Wenn dir keine klaren Namen einfallen, ist das ein Signal. Vielleicht sind die Anforderungen unvollständig, oder du vermischst mehrere Features. Gute Namen wie Loading, Content, Empty, Error oder Saving helfen dir, den Lebenszyklus des Features zu verstehen. In Compose ist das besonders nützlich, weil die UI aus State entsteht und jede Änderung sichtbar werden kann.

Tests aus der Zerlegung ableiten

Leite Tests aus den Teilproblemen ab. Ein Unit-Test kann prüfen, dass eine leere Repository-Antwort zu Empty führt. Ein anderer Test kann prüfen, dass ein Fehler zu Error führt. Ein Compose-Test kann prüfen, dass bei Error eine Wiederholen-Aktion sichtbar ist. Du testest damit nicht jede Zeile, sondern das Verhalten, das aus den Anforderungen folgt.

Auch Debugging wird leichter. Wenn die UI falsch aussieht, prüfst du zuerst den State. Ist der State korrekt, liegt das Problem wahrscheinlich in der Darstellung. Ist der State falsch, schaust du in ViewModel, Use Case oder Repository. Diese Richtung spart Zeit, weil du nicht wahllos durch die App springst.

Fazit

Problem Decomposition ist eine grundlegende Fähigkeit, die deine Android-Arbeit ruhiger und prüfbarer macht. Du zerlegst ein Feature in Daten, UI, State, Fehler und Tests, bevor du dich in Implementierungsdetails verlierst. Übe das bewusst mit einem kleinen Compose-Feature: Schreibe erst die fünf Teilbereiche auf, implementiere dann den UI-State, prüfe ihn mit einem Fake-Repository und lass den Code im Review danach beurteilen, ob jede Verantwortung an einer nachvollziehbaren Stelle liegt.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

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