Theming Architecture: Visuelle Entscheidungen zentral verwalten
Material Design Tokens und Dynamic Color zentralisieren alle visuellen Werte deiner App. Das hält das UI konsistent und macht Redesigns zum Einzeiler.
Wer eine Android-App mit mehr als zwei Screens entwickelt, kennt das Problem: Farben und Schriftgrößen verteilen sich über Dutzende Dateien, bis Color.parseColor("#1A73E8") an sieben verschiedenen Stellen auftaucht. Theming Architecture löst dieses Problem, indem sie alle visuellen Entscheidungen in ein einziges, klar strukturiertes System überführt – und dieses System dann sauber durch die gesamte App propagiert.
Was ist das?
Theming Architecture beschreibt das Prinzip, sämtliche Designentscheidungen – Farben, Typografie, Abstände und Formen – an einem zentralen Ort zu definieren und von dort aus konsistent durch die App zu verteilen. In der modernen Android-Entwicklung mit Jetpack Compose baut dieses System auf Material Design 3 (M3) auf.
Das Fundament bilden Design Tokens: benannte Abstraktionen über konkreten Werten. Statt Color(0xFF1A73E8) arbeitest du mit MaterialTheme.colorScheme.primary. Der eigentliche Farbwert lebt im Theme-Objekt; alle Composables greifen nur noch über semantische Namen darauf zu.
Drei Token-Ebenen strukturieren das System:
- Global Tokens – Rohwerte der Palette, z. B.
Blue40 = Color(0xFF1565C0) - Alias Tokens – Semantische Namen, die auf globale Tokens zeigen, z. B.
primary = Blue40 - Component Tokens – Komponentenspezifische Überschreibungen, z. B.
ButtonContainerColor
Dieses Drei-Ebenen-Modell macht Änderungen planbar: Willst du die Primärfarbe der gesamten App anpassen, änderst du genau einen Alias-Token – alles andere folgt automatisch.
Wie funktioniert es?
Jetpack Compose bietet mit MaterialTheme eine CompositionLocal-basierte Infrastruktur, über die Theme-Werte den gesamten Compose-Baum hinunterreichen, ohne explizit als Parameter weitergegeben zu werden.
ColorScheme und Dynamic Color
MaterialTheme erwartet ein ColorScheme-Objekt mit 30 semantischen Farbrollen (Primary, Secondary, Tertiary, Surface, Error und ihre Varianten). Du erstellst es entweder manuell mit lightColorScheme() / darkColorScheme() oder lässt das System via Dynamic Color automatisch eine passende Palette aus dem Gerätewallpaper ableiten:
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}
Das Theme wird einmalig ganz oben im Compose-Baum – typischerweise in MainActivity – aufgerufen. Alle darunter liegenden Composables lesen Werte über MaterialTheme.colorScheme.primary oder MaterialTheme.typography.titleLarge aus, ohne das Theme selbst zu kennen.
Typography und Shape
Neben Farben verwaltet MaterialTheme auch Typography (15 Textstile von displayLarge bis labelSmall) und Shapes (Abrundungsgrade für Komponenten). Beide funktionieren nach demselben Muster: einmal im Theme definiert, überall per MaterialTheme.typography.* bzw. MaterialTheme.shapes.* konsumiert.
In der Praxis
Theme-Dateien richtig organisieren
Lege eine eigene Datei ui/theme/Theme.kt an und definiere dort alle Bestandteile: die beiden ColorScheme-Objekte für Hell- und Dunkelmodus, das Typography-Objekt und das AppTheme-Composable. Das Material Theme Builder-Tool auf m3.material.io rechnet Markenfarben automatisch in ein vollständiges M3-Schema um – das spart Zeit und vermeidet Kontrastfehler.
Typische Stolperfalle: Farben im Composable hard-coden
// ❌ Falsch – Farbwert direkt im Widget
Text(
text = "Willkommen",
color = Color(0xFF1A73E8)
)
// ✅ Richtig – Token aus dem Theme
Text(
text = "Willkommen",
color = MaterialTheme.colorScheme.primary
)
Sobald du Dynamic Color aktivierst oder das Theme für eine White-Label-Variante anpasst, muss beim falschen Ansatz jede hartcodierte Stelle manuell gefunden und geändert werden. Mit Tokens ist es ein Einzeiler im Theme-Objekt.
Dark Mode und Dynamic Color kombinieren
Dynamic Color steht erst ab Android 12 (API 31) zur Verfügung. Für ältere Geräte muss ein statisches Fallback-Schema bereitstehen – genau wie im Code-Beispiel oben gezeigt. Teste beide Pfade im Emulator: Wallpaper wechseln, prüfen ob alle Farbrollen korrekt übernommen werden, dann in den Dunkelmodus schalten und dasselbe wiederholen.
Theming in UI-Tests isolieren
In Compose-UI-Tests solltest du dein AppTheme immer explizit um den Testinhalt legen:
@Test
fun primaryButtonUsesThemeColor() {
composeTestRule.setContent {
AppTheme {
PrimaryButton(onClick = {})
}
}
// Assertion hier
}
Ohne diesen Wrapper greift der Test auf das Standard-Material-Theme zurück – deine eigenen Token-Überschreibungen werden nicht angewendet, was zu false positives führen kann.
Fazit
Theming Architecture ist kein Luxus für große Teams – sie ist die Voraussetzung dafür, dass eine App bei wachsender Codebasis wartbar bleibt. Wer Design Tokens konsequent einsetzt, kann Dark Mode, Dynamic Color und White-Label-Varianten aktivieren, ohne eine einzige Composable-Datei anzufassen. Nimm dir jetzt fünf Minuten und prüfe in deinem aktuellen Projekt: Gibt es Color(0xFF...) oder colorResource(R.color.*) direkt in Composables? Wenn ja, überführe drei davon in Theme-Tokens, aktiviere den Dunkelmodus und beobachte, wie das gesamte UI ohne weiteren Aufwand korrekt reagiert – das ist der schnellste Weg, den konkreten Wert dieser Architektur mit eigenen Augen zu sehen.